From b5f9b4aa842fde26f66dc618b73d067b74fe6881 Mon Sep 17 00:00:00 2001 From: Jan Nicklas Date: Fri, 13 Feb 2015 18:45:23 +0100 Subject: [PATCH] Add support for multiple input elements in a form-group fixes #22 and #35 --- src/showErrors.coffee | 55 +++++++++++++------ src/showErrors.js | 72 ++++++++++++++++++------- src/showErrors.min.js | 4 +- test/showErrors.spec.coffee | 77 +++++++++++++++++++++++++++ test/showErrors.spec.js | 102 +++++++++++++++++++++++++++++++++++- 5 files changed, 269 insertions(+), 41 deletions(-) diff --git a/src/showErrors.coffee b/src/showErrors.coffee index 4d66b4d..9f96fbd 100644 --- a/src/showErrors.coffee +++ b/src/showErrors.coffee @@ -16,36 +16,57 @@ showErrorsModule.directive 'showErrors', showSuccess linkFn = (scope, el, attrs, formCtrl) -> - blurred = false + blurred = {} + inputNames = [] options = scope.$eval attrs.showErrors showSuccess = getShowSuccess options - trigger = getTrigger options + trigger = getTrigger options - inputEl = el[0].querySelector '.form-control[name]' - inputNgEl = angular.element inputEl - inputName = $interpolate(inputNgEl.attr('name') || '')(scope) - unless inputName - throw "show-errors element has no child input elements with a 'name' attribute and a 'form-control' class" + inputElements = {} + for inputElement in el[0].querySelectorAll('.form-control[name]') + if inputName = $interpolate(inputElement.name || '')(scope) + (inputElements[inputName] or= []).push inputElement + + isBlurred = () -> + allBlurred = true + angular.forEach blurred, (elementBlurred) -> + allBlurred = allBlurred && elementBlurred + allBlurred + + isValid = () -> + allValid = true + angular.forEach inputNames, (inputName) -> + allValid = allValid && formCtrl[inputName].$valid + allValid - inputNgEl.bind trigger, -> - blurred = true - toggleClasses formCtrl[inputName].$invalid + angular.forEach inputElements, (inputEl, inputName) -> + inputNames.push inputName + blurred[inputName] = false + inputNgEl = angular.element inputEl + inputNgEl.bind trigger, -> + blurred[inputName] = true + if isBlurred() + toggleClasses !isValid() - scope.$watch -> - formCtrl[inputName] && formCtrl[inputName].$invalid - , (invalid) -> - return if !blurred - toggleClasses invalid + scope.$watch -> + formCtrl[inputName] && formCtrl[inputName].$invalid + , (invalid) -> + return if !isBlurred() + toggleClasses !isValid() + + unless inputNames.length + throw "show-errors element has no child input elements with a 'name' attribute and a 'form-control' class" scope.$on 'show-errors-check-validity', -> - toggleClasses formCtrl[inputName].$invalid + toggleClasses !isValid() scope.$on 'show-errors-reset', -> $timeout -> # want to run this after the current digest cycle el.removeClass 'has-error' el.removeClass 'has-success' - blurred = false + angular.forEach inputNames, (inputName) -> + blurred[inputName] = false , 0, false toggleClasses = (invalid) -> diff --git a/src/showErrors.js b/src/showErrors.js index 934f59b..51d287d 100644 --- a/src/showErrors.js +++ b/src/showErrors.js @@ -23,37 +23,69 @@ return showSuccess; }; linkFn = function(scope, el, attrs, formCtrl) { - var blurred, inputEl, inputName, inputNgEl, options, showSuccess, toggleClasses, trigger; - blurred = false; + var blurred, inputElement, inputElements, inputName, inputNames, isBlurred, isValid, options, showSuccess, toggleClasses, trigger, _i, _len, _ref; + blurred = {}; + inputNames = []; options = scope.$eval(attrs.showErrors); showSuccess = getShowSuccess(options); trigger = getTrigger(options); - inputEl = el[0].querySelector('.form-control[name]'); - inputNgEl = angular.element(inputEl); - inputName = $interpolate(inputNgEl.attr('name') || '')(scope); - if (!inputName) { - throw "show-errors element has no child input elements with a 'name' attribute and a 'form-control' class"; - } - inputNgEl.bind(trigger, function() { - blurred = true; - return toggleClasses(formCtrl[inputName].$invalid); - }); - scope.$watch(function() { - return formCtrl[inputName] && formCtrl[inputName].$invalid; - }, function(invalid) { - if (!blurred) { - return; + inputElements = {}; + _ref = el[0].querySelectorAll('.form-control[name]'); + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + inputElement = _ref[_i]; + if (inputName = $interpolate(inputElement.name || '')(scope)) { + (inputElements[inputName] || (inputElements[inputName] = [])).push(inputElement); } - return toggleClasses(invalid); + } + isBlurred = function() { + var allBlurred; + allBlurred = true; + angular.forEach(blurred, function(elementBlurred) { + return allBlurred = allBlurred && elementBlurred; + }); + return allBlurred; + }; + isValid = function() { + var allValid; + allValid = true; + angular.forEach(inputNames, function(inputName) { + return allValid = allValid && formCtrl[inputName].$valid; + }); + return allValid; + }; + angular.forEach(inputElements, function(inputEl, inputName) { + var inputNgEl; + inputNames.push(inputName); + blurred[inputName] = false; + inputNgEl = angular.element(inputEl); + inputNgEl.bind(trigger, function() { + blurred[inputName] = true; + if (isBlurred()) { + return toggleClasses(!isValid()); + } + }); + return scope.$watch(function() { + return formCtrl[inputName] && formCtrl[inputName].$invalid; + }, function(invalid) { + if (!isBlurred()) { + return; + } + return toggleClasses(!isValid()); + }); }); + if (!inputNames.length) { + throw "show-errors element has no child input elements with a 'name' attribute and a 'form-control' class"; + } scope.$on('show-errors-check-validity', function() { - return toggleClasses(formCtrl[inputName].$invalid); + return toggleClasses(!isValid()); }); scope.$on('show-errors-reset', function() { return $timeout(function() { el.removeClass('has-error'); el.removeClass('has-success'); - return blurred = false; + return angular.forEach(inputNames, function(inputName) { + return blurred[inputName] = false; + }); }, 0, false); }); return toggleClasses = function(invalid) { diff --git a/src/showErrors.min.js b/src/showErrors.min.js index 72c5343..1bced51 100644 --- a/src/showErrors.min.js +++ b/src/showErrors.min.js @@ -1,2 +1,2 @@ -/*! angular-bootstrap-show-errors (version 2.3.0) 2015-01-19 */ -(function(){var a;a=angular.module("ui.bootstrap.showErrors",[]),a.directive("showErrors",["$timeout","showErrorsConfig","$interpolate",function(a,b,c){var d,e,f;return e=function(a){var c;return c=b.trigger,a&&null!=a.trigger&&(c=a.trigger),c},d=function(a){var c;return c=b.showSuccess,a&&null!=a.showSuccess&&(c=a.showSuccess),c},f=function(b,f,g,h){var i,j,k,l,m,n,o,p;if(i=!1,m=b.$eval(g.showErrors),n=d(m),p=e(m),j=f[0].querySelector(".form-control[name]"),l=angular.element(j),k=c(l.attr("name")||"")(b),!k)throw"show-errors element has no child input elements with a 'name' attribute and a 'form-control' class";return l.bind(p,function(){return i=!0,o(h[k].$invalid)}),b.$watch(function(){return h[k]&&h[k].$invalid},function(a){return i?o(a):void 0}),b.$on("show-errors-check-validity",function(){return o(h[k].$invalid)}),b.$on("show-errors-reset",function(){return a(function(){return f.removeClass("has-error"),f.removeClass("has-success"),i=!1},0,!1)}),o=function(a){return f.toggleClass("has-error",a),n?f.toggleClass("has-success",!a):void 0}},{restrict:"A",require:"^form",compile:function(a,b){if(-1===b.showErrors.indexOf("skipFormGroupCheck")&&!a.hasClass("form-group")&&!a.hasClass("input-group"))throw"show-errors element does not have the 'form-group' or 'input-group' class";return f}}}]),a.provider("showErrorsConfig",function(){var a,b;a=!1,b="blur",this.showSuccess=function(b){return a=b},this.trigger=function(a){return b=a},this.$get=function(){return{showSuccess:a,trigger:b}}})}).call(this); \ No newline at end of file +/*! angular-bootstrap-show-errors (version 2.3.0) 2015-02-13 */ +(function(){var a;a=angular.module("ui.bootstrap.showErrors",[]),a.directive("showErrors",["$timeout","showErrorsConfig","$interpolate",function(a,b,c){var d,e,f;return e=function(a){var c;return c=b.trigger,a&&null!=a.trigger&&(c=a.trigger),c},d=function(a){var c;return c=b.showSuccess,a&&null!=a.showSuccess&&(c=a.showSuccess),c},f=function(b,f,g,h){var i,j,k,l,m,n,o,p,q,r,s,t,u,v;for(i={},m=[],p=b.$eval(g.showErrors),q=d(p),s=e(p),k={},v=f[0].querySelectorAll(".form-control[name]"),t=0,u=v.length;u>t;t++)j=v[t],(l=c(j.name||"")(b))&&(k[l]||(k[l]=[])).push(j);if(n=function(){var a;return a=!0,angular.forEach(i,function(b){return a=a&&b}),a},o=function(){var a;return a=!0,angular.forEach(m,function(b){return a=a&&h[b].$valid}),a},angular.forEach(k,function(a,c){var d;return m.push(c),i[c]=!1,d=angular.element(a),d.bind(s,function(){return i[c]=!0,n()?r(!o()):void 0}),b.$watch(function(){return h[c]&&h[c].$invalid},function(){return n()?r(!o()):void 0})}),!m.length)throw"show-errors element has no child input elements with a 'name' attribute and a 'form-control' class";return b.$on("show-errors-check-validity",function(){return r(!o())}),b.$on("show-errors-reset",function(){return a(function(){return f.removeClass("has-error"),f.removeClass("has-success"),angular.forEach(m,function(a){return i[a]=!1})},0,!1)}),r=function(a){return f.toggleClass("has-error",a),q?f.toggleClass("has-success",!a):void 0}},{restrict:"A",require:"^form",compile:function(a,b){if(-1===b.showErrors.indexOf("skipFormGroupCheck")&&!a.hasClass("form-group")&&!a.hasClass("input-group"))throw"show-errors element does not have the 'form-group' or 'input-group' class";return f}}}]),a.provider("showErrorsConfig",function(){var a,b;a=!1,b="blur",this.showSuccess=function(b){return a=b},this.trigger=function(a){return b=a},this.$get=function(){return{showSuccess:a,trigger:b}}})}).call(this); \ No newline at end of file diff --git a/test/showErrors.spec.coffee b/test/showErrors.spec.coffee index 5ead727..f750d12 100644 --- a/test/showErrors.spec.coffee +++ b/test/showErrors.spec.coffee @@ -21,6 +21,10 @@ describe 'showErrors', ->
+
+ + +
' )($scope) angular.element(document.body).append el @@ -227,6 +231,65 @@ describe 'showErrors', -> $timeout.flush() expectLastNameFormGroupHasSuccessClass(el).toBe false + describe 'multiple inputs', -> + + describe '$dirty && one $invalid && all blurred', -> + it 'has-error is present', -> + el = compileEl() + $scope.userForm.zip.$setViewValue invalidName + $scope.userForm.city.$setViewValue validName + angular.element(zipEl(el)).triggerHandler 'blur' + angular.element(cityEl(el)).triggerHandler 'blur' + expectAddressFormGroupHasErrorClass(el).toBe true + + describe '$dirty && all $invalid && all blurred', -> + it 'has-error is present', -> + el = compileEl() + $scope.userForm.zip.$setViewValue invalidName + $scope.userForm.city.$setViewValue invalidName + angular.element(zipEl(el)).triggerHandler 'blur' + angular.element(cityEl(el)).triggerHandler 'blur' + expectAddressFormGroupHasErrorClass(el).toBe true + + describe '$dirty && all $valid && all blurred', -> + it 'has-error is absent', -> + el = compileEl() + $scope.userForm.zip.$setViewValue validName + $scope.userForm.city.$setViewValue validName + angular.element(zipEl(el)).triggerHandler 'blur' + angular.element(cityEl(el)).triggerHandler 'blur' + expectAddressFormGroupHasErrorClass(el).toBe false + + describe '$dirty && all $invalid && one blurred', -> + it 'has-error is absent', -> + el = compileEl() + $scope.userForm.zip.$setViewValue invalidName + $scope.userForm.city.$setViewValue invalidName + angular.element(zipEl(el)).triggerHandler 'blur' + expectAddressFormGroupHasErrorClass(el).toBe false + + describe '$dirty && one $invalid && one blurred', -> + it 'has-error is absent', -> + el = compileEl() + $scope.userForm.zip.$setViewValue validName + $scope.userForm.city.$setViewValue invalidName + angular.element(zipEl(el)).triggerHandler 'blur' + expectAddressFormGroupHasErrorClass(el).toBe false + + describe '$dirty && all $invalid && none blurred', -> + it 'has-error is absent', -> + el = compileEl() + $scope.userForm.zip.$setViewValue invalidName + $scope.userForm.city.$setViewValue invalidName + expectAddressFormGroupHasErrorClass(el).toBe false + + describe '$dirty && one $invalid && none blurred', -> + it 'has-error is absent', -> + el = compileEl() + $scope.userForm.zip.$setViewValue validName + $scope.userForm.city.$setViewValue invalidName + expectAddressFormGroupHasErrorClass(el).toBe false + describe 'showErrorsConfig', -> $compile = undefined $scope = undefined @@ -307,6 +370,12 @@ firstNameEl = (el) -> lastNameEl = (el) -> find el, '[name=lastName]' +zipEl = (el) -> + find el, '[name=zip]' + +cityEl = (el) -> + find el, '[name=city]' + expectFormGroupHasErrorClass = (el) -> formGroup = el[0].querySelector '[id=first-name-group]' expect angular.element(formGroup).hasClass('has-error') @@ -319,6 +388,10 @@ expectLastNameFormGroupHasSuccessClass = (el) -> formGroup = el[0].querySelector '[id=last-name-group]' expect angular.element(formGroup).hasClass('has-success') +expectAddressFormGroupHasSuccessClass = (el) -> + formGroup = el[0].querySelector '[id=address-group]' + expect angular.element(formGroup).hasClass('has-success') + expectFirstNameFormGroupHasErrorClass = (el) -> formGroup = el[0].querySelector '[id=first-name-group]' expect angular.element(formGroup).hasClass('has-error') @@ -326,3 +399,7 @@ expectFirstNameFormGroupHasErrorClass = (el) -> expectLastNameFormGroupHasErrorClass = (el) -> formGroup = el[0].querySelector '[id=last-name-group]' expect angular.element(formGroup).hasClass('has-error') + +expectAddressFormGroupHasErrorClass = (el) -> + formGroup = el[0].querySelector '[id=address-group]' + expect angular.element(formGroup).hasClass('has-error') \ No newline at end of file diff --git a/test/showErrors.spec.js b/test/showErrors.spec.js index 4186e2d..0214a53 100644 --- a/test/showErrors.spec.js +++ b/test/showErrors.spec.js @@ -1,5 +1,5 @@ (function() { - var expectFirstNameFormGroupHasErrorClass, expectFirstNameFormGroupHasSuccessClass, expectFormGroupHasErrorClass, expectLastNameFormGroupHasErrorClass, expectLastNameFormGroupHasSuccessClass, find, firstNameEl, lastNameEl; + var cityEl, expectAddressFormGroupHasErrorClass, expectAddressFormGroupHasSuccessClass, expectFirstNameFormGroupHasErrorClass, expectFirstNameFormGroupHasSuccessClass, expectFormGroupHasErrorClass, expectLastNameFormGroupHasErrorClass, expectLastNameFormGroupHasSuccessClass, find, firstNameEl, lastNameEl, zipEl; describe('showErrors', function() { var $compile, $scope, $timeout, compileEl, invalidName, validName; @@ -23,6 +23,10 @@
\ \
\ +
\ + \ + \ +
\ ')($scope); angular.element(document.body).append(el); $scope.$digest(); @@ -213,7 +217,7 @@ return expectFormGroupHasErrorClass(el).toBe(false); }); }); - return describe('{showSuccess: true} option', function() { + describe('{showSuccess: true} option', function() { describe('$pristine && $valid', function() { return it('has-success is absent', function() { var el; @@ -277,6 +281,79 @@ }); }); }); + return describe('multiple inputs', function() { + describe('$dirty && one $invalid && all blurred', function() { + return it('has-error is present', function() { + var el; + el = compileEl(); + $scope.userForm.zip.$setViewValue(invalidName); + $scope.userForm.city.$setViewValue(validName); + angular.element(zipEl(el)).triggerHandler('blur'); + angular.element(cityEl(el)).triggerHandler('blur'); + return expectAddressFormGroupHasErrorClass(el).toBe(true); + }); + }); + describe('$dirty && all $invalid && all blurred', function() { + return it('has-error is present', function() { + var el; + el = compileEl(); + $scope.userForm.zip.$setViewValue(invalidName); + $scope.userForm.city.$setViewValue(invalidName); + angular.element(zipEl(el)).triggerHandler('blur'); + angular.element(cityEl(el)).triggerHandler('blur'); + return expectAddressFormGroupHasErrorClass(el).toBe(true); + }); + }); + describe('$dirty && all $valid && all blurred', function() { + return it('has-error is absent', function() { + var el; + el = compileEl(); + $scope.userForm.zip.$setViewValue(validName); + $scope.userForm.city.$setViewValue(validName); + angular.element(zipEl(el)).triggerHandler('blur'); + angular.element(cityEl(el)).triggerHandler('blur'); + return expectAddressFormGroupHasErrorClass(el).toBe(false); + }); + }); + describe('$dirty && all $invalid && one blurred', function() { + return it('has-error is absent', function() { + var el; + el = compileEl(); + $scope.userForm.zip.$setViewValue(invalidName); + $scope.userForm.city.$setViewValue(invalidName); + angular.element(zipEl(el)).triggerHandler('blur'); + return expectAddressFormGroupHasErrorClass(el).toBe(false); + }); + }); + describe('$dirty && one $invalid && one blurred', function() { + return it('has-error is absent', function() { + var el; + el = compileEl(); + $scope.userForm.zip.$setViewValue(validName); + $scope.userForm.city.$setViewValue(invalidName); + angular.element(zipEl(el)).triggerHandler('blur'); + return expectAddressFormGroupHasErrorClass(el).toBe(false); + }); + }); + describe('$dirty && all $invalid && none blurred', function() { + return it('has-error is absent', function() { + var el; + el = compileEl(); + $scope.userForm.zip.$setViewValue(invalidName); + $scope.userForm.city.$setViewValue(invalidName); + return expectAddressFormGroupHasErrorClass(el).toBe(false); + }); + }); + return describe('$dirty && one $invalid && none blurred', function() { + return it('has-error is absent', function() { + var el; + el = compileEl(); + $scope.userForm.zip.$setViewValue(validName); + $scope.userForm.city.$setViewValue(invalidName); + return expectAddressFormGroupHasErrorClass(el).toBe(false); + }); + }); + }); }); describe('showErrorsConfig', function() { @@ -333,6 +410,7 @@ el = compileEl(); $scope.userForm.firstName.$setViewValue(validName); angular.element(firstNameEl(el)).triggerHandler('blur'); + angular.element(firstNameEl(el)).triggerHandler('blur'); $scope.$digest(); return expectFirstNameFormGroupHasSuccessClass(el).toBe(false); }); @@ -374,6 +452,14 @@ return find(el, '[name=lastName]'); }; + zipEl = function(el) { + return find(el, '[name=zip]'); + }; + + cityEl = function(el) { + return find(el, '[name=city]'); + }; + expectFormGroupHasErrorClass = function(el) { var formGroup; formGroup = el[0].querySelector('[id=first-name-group]'); @@ -392,6 +478,12 @@ return expect(angular.element(formGroup).hasClass('has-success')); }; + expectAddressFormGroupHasSuccessClass = function(el) { + var formGroup; + formGroup = el[0].querySelector('[id=address-group]'); + return expect(angular.element(formGroup).hasClass('has-success')); + }; + expectFirstNameFormGroupHasErrorClass = function(el) { var formGroup; formGroup = el[0].querySelector('[id=first-name-group]'); @@ -404,4 +496,10 @@ return expect(angular.element(formGroup).hasClass('has-error')); }; + expectAddressFormGroupHasErrorClass = function(el) { + var formGroup; + formGroup = el[0].querySelector('[id=address-group]'); + return expect(angular.element(formGroup).hasClass('has-error')); + }; + }).call(this);