From 496a67c10ef0554735bee31a2ca83852f81a1048 Mon Sep 17 00:00:00 2001 From: Peter Bacon Darwin Date: Mon, 14 Sep 2015 14:41:58 +0100 Subject: [PATCH 1/2] test(ngModel): remove jankiness test It is no longer appropriate to test this here as $animate takes care of queueing up CSS class changes into a single update. --- test/ng/directive/ngModelSpec.js | 40 -------------------------------- 1 file changed, 40 deletions(-) diff --git a/test/ng/directive/ngModelSpec.js b/test/ng/directive/ngModelSpec.js index b45ea2e0c0e0..9c1e6bba16f2 100644 --- a/test/ng/directive/ngModelSpec.js +++ b/test/ng/directive/ngModelSpec.js @@ -1182,46 +1182,6 @@ describe('ngModel', function() { })); - it('should minimize janky setting of classes during $validate() and ngModelWatch', inject(function($animate, $compile, $rootScope) { - var addClass = $animate.addClass; - var removeClass = $animate.removeClass; - var addClassCallCount = 0; - var removeClassCallCount = 0; - var input; - $animate.addClass = function(element, className) { - if (input && element[0] === input[0]) ++addClassCallCount; - return addClass.call($animate, element, className); - }; - - $animate.removeClass = function(element, className) { - if (input && element[0] === input[0]) ++removeClassCallCount; - return removeClass.call($animate, element, className); - }; - - dealoc(element); - - $rootScope.value = "123456789"; - element = $compile( - '
' + - '' + - '
' - )($rootScope); - - var form = $rootScope.form; - input = element.children().eq(0); - - $rootScope.$digest(); - - expect(input).toBeValid(); - expect(input).not.toHaveClass('ng-invalid-maxlength'); - expect(input).toHaveClass('ng-valid-maxlength'); - expect(addClassCallCount).toBe(1); - expect(removeClassCallCount).toBe(0); - - dealoc(element); - })); - - it('should always use the most recent $viewValue for validation', function() { ctrl.$parsers.push(function(value) { if (value && value.substr(-1) === 'b') { From 630280c7fb04a83208d09c97c2efb81be3a3db74 Mon Sep 17 00:00:00 2001 From: Peter Bacon Darwin Date: Mon, 14 Sep 2015 17:33:58 +0100 Subject: [PATCH 2/2] feat(ngModel): provide ng-empty and ng-not-empty CSS classes If the `$viewValue` is empty then the `ng-empty` CSS class is applied to the input. Conversely, if it is not empty the `ng-not-empty` CSS class is applied. Emptiness is ascertained by calling `NgModelController.$isEmpty()` Closes #10050 Closes #12848 --- src/ng/directive/ngModel.js | 25 ++++++++++++++++++++++--- test/helpers/matchers.js | 2 ++ test/ng/directive/ngModelSpec.js | 28 ++++++++++++++++++++++++++-- 3 files changed, 50 insertions(+), 5 deletions(-) diff --git a/src/ng/directive/ngModel.js b/src/ng/directive/ngModel.js index 034c386a5527..4978976163a6 100644 --- a/src/ng/directive/ngModel.js +++ b/src/ng/directive/ngModel.js @@ -14,7 +14,9 @@ var VALID_CLASS = 'ng-valid', DIRTY_CLASS = 'ng-dirty', UNTOUCHED_CLASS = 'ng-untouched', TOUCHED_CLASS = 'ng-touched', - PENDING_CLASS = 'ng-pending'; + PENDING_CLASS = 'ng-pending', + EMPTY_CLASS = 'ng-empty', + NOT_EMPTY_CLASS = 'ng-not-empty'; var ngModelMinErr = minErr('ngModel'); @@ -316,6 +318,17 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ return isUndefined(value) || value === '' || value === null || value !== value; }; + this.$$updateEmptyClasses = function(value) { + if (ctrl.$isEmpty(value)) { + $animate.removeClass($element, NOT_EMPTY_CLASS); + $animate.addClass($element, EMPTY_CLASS); + } else { + $animate.removeClass($element, EMPTY_CLASS); + $animate.addClass($element, NOT_EMPTY_CLASS); + } + }; + + var currentValidationRunId = 0; /** @@ -652,6 +665,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ if (ctrl.$$lastCommittedViewValue === viewValue && (viewValue !== '' || !ctrl.$$hasNativeValidators)) { return; } + ctrl.$$updateEmptyClasses(viewValue); ctrl.$$lastCommittedViewValue = viewValue; // change to dirty @@ -834,6 +848,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ viewValue = formatters[idx](viewValue); } if (ctrl.$viewValue !== viewValue) { + ctrl.$$updateEmptyClasses(viewValue); ctrl.$viewValue = ctrl.$$lastCommittedViewValue = viewValue; ctrl.$render(); @@ -864,7 +879,8 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * require. * - Providing validation behavior (i.e. required, number, email, url). * - Keeping the state of the control (valid/invalid, dirty/pristine, touched/untouched, validation errors). - * - Setting related css classes on the element (`ng-valid`, `ng-invalid`, `ng-dirty`, `ng-pristine`, `ng-touched`, `ng-untouched`) including animations. + * - Setting related css classes on the element (`ng-valid`, `ng-invalid`, `ng-dirty`, `ng-pristine`, `ng-touched`, + * `ng-untouched`, `ng-empty`, `ng-not-empty`) including animations. * - Registering the control with its parent {@link ng.directive:form form}. * * Note: `ngModel` will try to bind to the property given by evaluating the expression on the @@ -905,13 +921,16 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * - `ng-touched`: the control has been blurred * - `ng-untouched`: the control hasn't been blurred * - `ng-pending`: any `$asyncValidators` are unfulfilled + * - `ng-empty`: the view does not contain a value or the value is deemed "empty", as defined + * by the {@link ngModel.NgModelController#$isEmpty} method + * - `ng-not-empty`: the view contains a non-empty value * * Keep in mind that ngAnimate can detect each of these classes when added and removed. * * ## Animation Hooks * * Animations within models are triggered when any of the associated CSS classes are added and removed - * on the input element which is attached to the model. These classes are: `.ng-pristine`, `.ng-dirty`, + * on the input element which is attached to the model. These classes include: `.ng-pristine`, `.ng-dirty`, * `.ng-invalid` and `.ng-valid` as well as any other validations that are performed on the model itself. * The animations that are triggered within ngModel are similar to how they work in ngClass and * animations can be hooked into using CSS transitions, keyframes as well as JS animations. diff --git a/test/helpers/matchers.js b/test/helpers/matchers.js index 9599b2dcbfed..fdcb335509a8 100644 --- a/test/helpers/matchers.js +++ b/test/helpers/matchers.js @@ -37,6 +37,8 @@ beforeEach(function() { } this.addMatchers({ + toBeEmpty: cssMatcher('ng-empty', 'ng-not-empty'), + toBeNotEmpty: cssMatcher('ng-not-empty', 'ng-empty'), toBeInvalid: cssMatcher('ng-invalid', 'ng-valid'), toBeValid: cssMatcher('ng-valid', 'ng-invalid'), toBeDirty: cssMatcher('ng-dirty', 'ng-pristine'), diff --git a/test/ng/directive/ngModelSpec.js b/test/ng/directive/ngModelSpec.js index 9c1e6bba16f2..e725d495fe00 100644 --- a/test/ng/directive/ngModelSpec.js +++ b/test/ng/directive/ngModelSpec.js @@ -1328,6 +1328,28 @@ describe('ngModel', function() { describe('CSS classes', function() { var EMAIL_REGEXP = /^[a-z0-9!#$%&'*+\/=?^_`{|}~.-]+@[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i; + it('should set ng-empty or ng-not-empty when the view value changes', + inject(function($compile, $rootScope, $sniffer) { + + var element = $compile('')($rootScope); + + $rootScope.$digest(); + expect(element).toBeEmpty(); + + $rootScope.value = 'XXX'; + $rootScope.$digest(); + expect(element).toBeNotEmpty(); + + element.val(''); + browserTrigger(element, $sniffer.hasEvent('input') ? 'input' : 'change'); + expect(element).toBeEmpty(); + + element.val('YYY'); + browserTrigger(element, $sniffer.hasEvent('input') ? 'input' : 'change'); + expect(element).toBeNotEmpty(); + })); + + it('should set css classes (ng-valid, ng-invalid, ng-pristine, ng-dirty, ng-untouched, ng-touched)', inject(function($compile, $rootScope, $sniffer) { var element = $compile('')($rootScope); @@ -1693,8 +1715,10 @@ describe('ngModel', function() { model.$setViewValue('some dirty value'); var animations = findElementAnimations(input, $animate.queue); - assertValidAnimation(animations[0], 'removeClass', 'ng-pristine'); - assertValidAnimation(animations[1], 'addClass', 'ng-dirty'); + assertValidAnimation(animations[0], 'removeClass', 'ng-empty'); + assertValidAnimation(animations[1], 'addClass', 'ng-not-empty'); + assertValidAnimation(animations[2], 'removeClass', 'ng-pristine'); + assertValidAnimation(animations[3], 'addClass', 'ng-dirty'); }));