diff options
| -rw-r--r-- | src/directive/form.js | 42 | ||||
| -rw-r--r-- | src/directive/input.js | 53 | ||||
| -rw-r--r-- | test/directive/formSpec.js | 42 | ||||
| -rw-r--r-- | test/directive/inputSpec.js | 39 |
4 files changed, 116 insertions, 60 deletions
diff --git a/src/directive/form.js b/src/directive/form.js index 6bb1b4d6..47274589 100644 --- a/src/directive/form.js +++ b/src/directive/form.js @@ -35,6 +35,7 @@ FormController.$inject = ['name', '$element', '$attrs']; function FormController(name, element, attrs) { var form = this, parentForm = element.parent().inheritedData('$formController') || nullFormCtrl, + invalidCount = 0, // used to easily determine if we are valid errors = form.$error = {}; // init state @@ -49,11 +50,27 @@ function FormController(name, element, attrs) { parentForm.$addControl(form); + // Setup initial state of the control + element.addClass(PRISTINE_CLASS); + toggleValidCss(true); + + // convenience method for easy toggling of classes + function toggleValidCss(isValid, validationErrorKey) { + validationErrorKey = validationErrorKey ? '-' + snake_case(validationErrorKey, '-') : ''; + element. + removeClass((isValid ? INVALID_CLASS : VALID_CLASS) + validationErrorKey). + addClass((isValid ? VALID_CLASS : INVALID_CLASS) + validationErrorKey); + } + + if (parentForm) { + parentForm.$addControl(form); + } + form.$addControl = function(control) { if (control.$name && !form.hasOwnProperty(control.$name)) { form[control.$name] = control; } - } + }; form.$removeControl = function(control) { if (control.$name && form[control.$name] === control) { @@ -66,11 +83,15 @@ function FormController(name, element, attrs) { if (isValid) { cleanupControlErrors(errors[validationToken], validationToken, control); - if (equals(errors, {})) { + if (!invalidCount) { + toggleValidCss(isValid); form.$valid = true; form.$invalid = false; } } else { + if (!invalidCount) { + toggleValidCss(isValid); + } addControlError(validationToken, control); form.$valid = false; @@ -79,16 +100,19 @@ function FormController(name, element, attrs) { }; form.$setDirty = function() { + element.removeClass(PRISTINE_CLASS).addClass(DIRTY_CLASS); form.$dirty = true; form.$pristine = false; - } + }; function cleanupControlErrors(queue, validationToken, control) { if (queue) { control = control || this; // so that we can be used in forEach; arrayRemove(queue, control); if (!queue.length) { - delete errors[validationToken]; + invalidCount--; + errors[validationToken] = false; + toggleValidCss(true, validationToken); parentForm.$setValidity(validationToken, true, form); } } @@ -100,6 +124,8 @@ function FormController(name, element, attrs) { if (includes(queue, control)) return; } else { errors[validationToken] = queue = []; + invalidCount++; + toggleValidCss(false, validationToken); parentForm.$setValidity(validationToken, false, form); } queue.push(control); @@ -211,14 +237,6 @@ var formDirective = [function() { if (!attr.action) event.preventDefault(); }); - forEach(['valid', 'invalid', 'dirty', 'pristine'], function(name) { - scope.$watch(function() { - return controller['$' + name]; - }, function(value) { - formElement[value ? 'addClass' : 'removeClass']('ng-' + name); - }); - }); - var parentFormCtrl = formElement.parent().inheritedData('$formController'); if (parentFormCtrl) { formElement.bind('$destroy', function() { diff --git a/src/directive/input.js b/src/directive/input.js index a92ad306..740f3ba6 100644 --- a/src/directive/input.js +++ b/src/directive/input.js @@ -719,6 +719,10 @@ var inputDirective = [function() { }; }]; +var VALID_CLASS = 'ng-valid', + INVALID_CLASS = 'ng-invalid', + PRISTINE_CLASS = 'ng-pristine', + DIRTY_CLASS = 'ng-dirty'; /** * @ngdoc object @@ -749,7 +753,6 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', 'ngModel', '$e this.$parsers = []; this.$formatters = []; this.$viewChangeListeners = []; - this.$error = {}; this.$pristine = true; this.$dirty = false; this.$valid = true; @@ -757,7 +760,22 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', 'ngModel', '$e this.$render = noop; this.$name = $attr.name; - var parentForm = $element.inheritedData('$formController') || nullFormCtrl; + var parentForm = $element.inheritedData('$formController') || nullFormCtrl, + invalidCount = 0, // used to easily determine if we are valid + $error = this.$error = {}; // keep invalid keys here + + + // Setup initial state of the control + $element.addClass(PRISTINE_CLASS); + toggleValidCss(true); + + // convenience method for easy toggling of classes + function toggleValidCss(isValid, validationErrorKey) { + validationErrorKey = validationErrorKey ? '-' + snake_case(validationErrorKey, '-') : ''; + $element. + removeClass((isValid ? INVALID_CLASS : VALID_CLASS) + validationErrorKey). + addClass((isValid ? VALID_CLASS : INVALID_CLASS) + validationErrorKey); + } /** * @ngdoc function @@ -770,22 +788,30 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', 'ngModel', '$e * * This method should be called by validators - i.e. the parser or formatter functions. * - * @param {string} validationErrorKey Name of the validator. + * @param {string} validationErrorKey Name of the validator. the `validationErrorKey` will assign + * to `$error[validationErrorKey]=isValid` so that it is available for data-binding. + * The `validationErrorKey` should be in camelCase and will get converted into dash-case + * for class name. Example: `myError` will result in `ng-valid-my-error` and `ng-invalid-my-error` + * class and can be bound to as `{{someForm.someControl.$error.myError}}` . * @param {boolean} isValid Whether the current state is valid (true) or invalid (false). */ this.$setValidity = function(validationErrorKey, isValid) { - - if (!isValid && this.$error[validationErrorKey]) return; - if (isValid && !this.$error[validationErrorKey]) return; + if ($error[validationErrorKey] === !isValid) return; if (isValid) { - delete this.$error[validationErrorKey]; - if (equals(this.$error, {})) { + if ($error[validationErrorKey]) invalidCount--; + $error[validationErrorKey] = false; + toggleValidCss(isValid); + if (!invalidCount) { + toggleValidCss(isValid, validationErrorKey); this.$valid = true; this.$invalid = false; } } else { - this.$error[validationErrorKey] = true; + if (!$error[validationErrorKey]) invalidCount++; + $error[validationErrorKey] = true; + toggleValidCss(isValid) + toggleValidCss(isValid, validationErrorKey); this.$invalid = true; this.$valid = false; } @@ -818,6 +844,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', 'ngModel', '$e if (this.$pristine) { this.$dirty = true; this.$pristine = false; + $element.removeClass(PRISTINE_CLASS).addClass(DIRTY_CLASS); parentForm.$setDirty(); } @@ -910,14 +937,6 @@ var ngModelDirective = [function() { formCtrl.$addControl(modelCtrl); - forEach(['valid', 'invalid', 'pristine', 'dirty'], function(name) { - scope.$watch(function() { - return modelCtrl['$' + name]; - }, function(value) { - element[value ? 'addClass' : 'removeClass']('ng-' + name); - }); - }); - element.bind('$destroy', function() { formCtrl.$removeControl(modelCtrl); }); diff --git a/test/directive/formSpec.js b/test/directive/formSpec.js index fbcf4aec..588b9f49 100644 --- a/test/directive/formSpec.js +++ b/test/directive/formSpec.js @@ -43,7 +43,7 @@ describe('form', function() { expect(form.$error.required).toEqual([control]); doc.find('input').remove(); - expect(form.$error.required).toBeUndefined(); + expect(form.$error.required).toBe(false); expect(form.alias).toBeUndefined(); }); @@ -124,8 +124,8 @@ describe('form', function() { expect(scope.firstName).toBe('val1'); expect(scope.lastName).toBe('val2'); - expect(scope.formA.$error.required).toBeUndefined(); - expect(scope.formB.$error.required).toBeUndefined(); + expect(scope.formA.$error.required).toBe(false); + expect(scope.formB.$error.required).toBe(false); }); @@ -169,8 +169,8 @@ describe('form', function() { expect(child.$error.MyError).toEqual([inputB]); inputB.$setValidity('MyError', true); - expect(parent.$error.MyError).toBeUndefined(); - expect(child.$error.MyError).toBeUndefined(); + expect(parent.$error.MyError).toBe(false); + expect(child.$error.MyError).toBe(false); }); @@ -192,7 +192,7 @@ describe('form', function() { expect(parent.child).toBeUndefined(); expect(scope.child).toBeUndefined(); - expect(parent.$error.required).toBeUndefined(); + expect(parent.$error.required).toBe(false); }); @@ -223,8 +223,8 @@ describe('form', function() { expect(parent.$error.myRule).toEqual([child]); input.$setValidity('myRule', true); - expect(parent.$error.myRule).toBeUndefined(); - expect(child.$error.myRule).toBeUndefined(); + expect(parent.$error.myRule).toBe(false); + expect(child.$error.myRule).toBe(false); }); }) @@ -244,20 +244,30 @@ describe('form', function() { it('should have ng-valid/ng-invalid css class', function() { expect(doc).toBeValid(); - control.$setValidity('ERROR', false); - scope.$apply(); + control.$setValidity('error', false); expect(doc).toBeInvalid(); + expect(doc.hasClass('ng-valid-error')).toBe(false); + expect(doc.hasClass('ng-invalid-error')).toBe(true); - control.$setValidity('ANOTHER', false); - scope.$apply(); + control.$setValidity('another', false); + expect(doc.hasClass('ng-valid-error')).toBe(false); + expect(doc.hasClass('ng-invalid-error')).toBe(true); + expect(doc.hasClass('ng-valid-another')).toBe(false); + expect(doc.hasClass('ng-invalid-another')).toBe(true); - control.$setValidity('ERROR', true); - scope.$apply(); + control.$setValidity('error', true); expect(doc).toBeInvalid(); + expect(doc.hasClass('ng-valid-error')).toBe(true); + expect(doc.hasClass('ng-invalid-error')).toBe(false); + expect(doc.hasClass('ng-valid-another')).toBe(false); + expect(doc.hasClass('ng-invalid-another')).toBe(true); - control.$setValidity('ANOTHER', true); - scope.$apply(); + control.$setValidity('another', true); expect(doc).toBeValid(); + expect(doc.hasClass('ng-valid-error')).toBe(true); + expect(doc.hasClass('ng-invalid-error')).toBe(false); + expect(doc.hasClass('ng-valid-another')).toBe(true); + expect(doc.hasClass('ng-invalid-another')).toBe(false); }); diff --git a/test/directive/inputSpec.js b/test/directive/inputSpec.js index 4bdba36a..a9aafd04 100644 --- a/test/directive/inputSpec.js +++ b/test/directive/inputSpec.js @@ -66,38 +66,39 @@ describe('NgModelController', function() { expect(ctrl.$error.required).toBe(true); ctrl.$setValidity('required', true); - expect(ctrl.$error.required).toBeUndefined(); + expect(ctrl.$error.required).toBe(false); }); it('should set valid/invalid', function() { - ctrl.$setValidity('FIRST', false); + ctrl.$setValidity('first', false); expect(ctrl.$valid).toBe(false); expect(ctrl.$invalid).toBe(true); - ctrl.$setValidity('SECOND', false); + ctrl.$setValidity('second', false); expect(ctrl.$valid).toBe(false); expect(ctrl.$invalid).toBe(true); - ctrl.$setValidity('SECOND', true); + ctrl.$setValidity('second', true); expect(ctrl.$valid).toBe(false); expect(ctrl.$invalid).toBe(true); - ctrl.$setValidity('FIRST', true); + ctrl.$setValidity('first', true); expect(ctrl.$valid).toBe(true); expect(ctrl.$invalid).toBe(false); }); it('should emit $valid only when $invalid', function() { - ctrl.$setValidity('ERROR', true); - expect(parentFormCtrl.$setValidity).not.toHaveBeenCalled(); + ctrl.$setValidity('error', true); + expect(parentFormCtrl.$setValidity).toHaveBeenCalledOnceWith('error', true, ctrl); + parentFormCtrl.$setValidity.reset(); - ctrl.$setValidity('ERROR', false); - expect(parentFormCtrl.$setValidity).toHaveBeenCalledOnceWith('ERROR', false, ctrl); + ctrl.$setValidity('error', false); + expect(parentFormCtrl.$setValidity).toHaveBeenCalledOnceWith('error', false, ctrl); parentFormCtrl.$setValidity.reset(); - ctrl.$setValidity('ERROR', true); - expect(parentFormCtrl.$setValidity).toHaveBeenCalledOnceWith('ERROR', true, ctrl); + ctrl.$setValidity('error', true); + expect(parentFormCtrl.$setValidity).toHaveBeenCalledOnceWith('error', true, ctrl); }); }); @@ -152,7 +153,7 @@ describe('NgModelController', function() { ctrl.$parsers.push(function() {return undefined;}); expect(ctrl.$modelValue).toBe('aaaa'); ctrl.$setViewValue('bbbb'); - expect(ctrl.$modelValue).toBeUndefined; + expect(ctrl.$modelValue).toBeUndefined(); }); @@ -241,22 +242,30 @@ describe('ng-model', function() { $rootScope.$digest(); expect(element).toBeValid(); expect(element).toBePristine(); + expect(element.hasClass('ng-valid-email')).toBe(true); + expect(element.hasClass('ng-invalid-email')).toBe(false); $rootScope.$apply(function() { $rootScope.value = 'invalid-email'; }); expect(element).toBeInvalid(); expect(element).toBePristine(); + expect(element.hasClass('ng-valid-email')).toBe(false); + expect(element.hasClass('ng-invalid-email')).toBe(true); element.val('invalid-again'); browserTrigger(element, 'blur'); expect(element).toBeInvalid(); expect(element).toBeDirty(); + expect(element.hasClass('ng-valid-email')).toBe(false); + expect(element.hasClass('ng-invalid-email')).toBe(true); element.val('vojta@google.com'); browserTrigger(element, 'blur'); expect(element).toBeValid(); expect(element).toBeDirty(); + expect(element.hasClass('ng-valid-email')).toBe(true); + expect(element.hasClass('ng-invalid-email')).toBe(false); dealoc(element); })); @@ -305,7 +314,7 @@ describe('input', function() { expect(scope.form.$error.required.length).toBe(1); inputElm.remove(); - expect(scope.form.$error.required).toBeUndefined(); + expect(scope.form.$error.required).toBe(false); }); @@ -605,7 +614,7 @@ describe('input', function() { expect(scope.email).toBe('vojta@google.com'); expect(inputElm).toBeValid(); - expect(widget.$error.email).toBeUndefined(); + expect(widget.$error.email).toBe(false); changeInputValueTo('invalid@'); expect(scope.email).toBeUndefined(); @@ -633,7 +642,7 @@ describe('input', function() { changeInputValueTo('http://www.something.com'); expect(scope.url).toBe('http://www.something.com'); expect(inputElm).toBeValid(); - expect(widget.$error.url).toBeUndefined(); + expect(widget.$error.url).toBe(false); changeInputValueTo('invalid.com'); expect(scope.url).toBeUndefined(); |
