diff --git a/css/angular.css b/css/angular.css index b9eda79e745d..81da6c4a111c 100644 --- a/css/angular.css +++ b/css/angular.css @@ -2,7 +2,7 @@ [ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak], .ng-cloak, .x-ng-cloak, -.ng-hide:not(.ng-animate) { +.ng-hide:not(.ng-hide-animate) { display: none !important; } diff --git a/src/ng/directive/ngShowHide.js b/src/ng/directive/ngShowHide.js index 7c4baa730abc..ebcc05b14111 100644 --- a/src/ng/directive/ngShowHide.js +++ b/src/ng/directive/ngShowHide.js @@ -1,5 +1,7 @@ 'use strict'; +var NG_HIDE_CLASS = 'ng-hide'; +var NG_HIDE_IN_PROGRESS_CLASS = 'ng-hide-animate'; /** * @ngdoc directive * @name ngShow @@ -161,7 +163,11 @@ var ngShowDirective = ['$animate', function($animate) { multiElement: true, link: function(scope, element, attr) { scope.$watch(attr.ngShow, function ngShowWatchAction(value){ - $animate[value ? 'removeClass' : 'addClass'](element, 'ng-hide'); + // we're adding a temporary, animation-specific class for ng-hide since this way + // we can control when the element is actually displayed on screen without having + // to have a global/greedy CSS selector that breaks when other animations are run. + // Read: https://github.com/angular/angular.js/issues/9103#issuecomment-58335845 + $animate[value ? 'removeClass' : 'addClass'](element, NG_HIDE_CLASS, NG_HIDE_IN_PROGRESS_CLASS); }); } }; @@ -316,7 +322,9 @@ var ngHideDirective = ['$animate', function($animate) { multiElement: true, link: function(scope, element, attr) { scope.$watch(attr.ngHide, function ngHideWatchAction(value){ - $animate[value ? 'addClass' : 'removeClass'](element, 'ng-hide'); + // The comment inside of the ngShowDirective explains why we add and + // remove a temporary class for the show/hide animation + $animate[value ? 'addClass' : 'removeClass'](element,NG_HIDE_CLASS, NG_HIDE_IN_PROGRESS_CLASS); }); } }; diff --git a/src/ngAnimate/animate.js b/src/ngAnimate/animate.js index a8cb6263a0cf..945667b7eee2 100644 --- a/src/ngAnimate/animate.js +++ b/src/ngAnimate/animate.js @@ -377,6 +377,7 @@ angular.module('ngAnimate', ['ng']) var forEach = angular.forEach; var selectors = $animateProvider.$$selectors; var isArray = angular.isArray; + var isString = angular.isString; var ELEMENT_NODE = 1; var NG_ANIMATE_STATE = '$$ngAnimateState'; @@ -467,6 +468,14 @@ angular.module('ngAnimate', ['ng']) return defer.promise; } + function parseAnimateOptions(options) { + // some plugin code may still be passing in the callback + // function as the last param for the $animate methods so + // it's best to only allow string or array values for now + if (isArray(options)) return options; + if (isString(options)) return [options]; + } + function resolveElementClasses(element, cache, runningAnimations) { runningAnimations = runningAnimations || {}; @@ -779,7 +788,8 @@ angular.module('ngAnimate', ['ng']) * @param {DOMElement} afterElement the sibling element (which is the previous element) of the element that will be the focus of the enter animation * @return {Promise} the animation callback promise */ - enter : function(element, parentElement, afterElement) { + enter : function(element, parentElement, afterElement, options) { + options = parseAnimateOptions(options); element = angular.element(element); parentElement = prepareElement(parentElement); afterElement = prepareElement(afterElement); @@ -787,7 +797,7 @@ angular.module('ngAnimate', ['ng']) classBasedAnimationsBlocked(element, true); $delegate.enter(element, parentElement, afterElement); return runAnimationPostDigest(function(done) { - return performAnimation('enter', 'ng-enter', stripCommentsFromElement(element), parentElement, afterElement, noop, done); + return performAnimation('enter', 'ng-enter', stripCommentsFromElement(element), parentElement, afterElement, noop, options, done); }); }, @@ -821,7 +831,8 @@ angular.module('ngAnimate', ['ng']) * @param {DOMElement} element the element that will be the focus of the leave animation * @return {Promise} the animation callback promise */ - leave : function(element) { + leave : function(element, options) { + options = parseAnimateOptions(options); element = angular.element(element); cancelChildAnimations(element); @@ -830,7 +841,7 @@ angular.module('ngAnimate', ['ng']) return runAnimationPostDigest(function(done) { return performAnimation('leave', 'ng-leave', stripCommentsFromElement(element), null, null, function() { $delegate.leave(element); - }, done); + }, options, done); }); }, @@ -867,7 +878,8 @@ angular.module('ngAnimate', ['ng']) * @param {DOMElement} afterElement the sibling element (which is the previous element) of the element that will be the focus of the move animation * @return {Promise} the animation callback promise */ - move : function(element, parentElement, afterElement) { + move : function(element, parentElement, afterElement, options) { + options = parseAnimateOptions(options); element = angular.element(element); parentElement = prepareElement(parentElement); afterElement = prepareElement(afterElement); @@ -876,7 +888,7 @@ angular.module('ngAnimate', ['ng']) classBasedAnimationsBlocked(element, true); $delegate.move(element, parentElement, afterElement); return runAnimationPostDigest(function(done) { - return performAnimation('move', 'ng-move', stripCommentsFromElement(element), parentElement, afterElement, noop, done); + return performAnimation('move', 'ng-move', stripCommentsFromElement(element), parentElement, afterElement, noop, options, done); }); }, @@ -909,8 +921,8 @@ angular.module('ngAnimate', ['ng']) * @param {string} className the CSS class that will be added to the element and then animated * @return {Promise} the animation callback promise */ - addClass : function(element, className) { - return this.setClass(element, className, []); + addClass : function(element, className, options) { + return this.setClass(element, className, [], options); }, /** @@ -942,8 +954,8 @@ angular.module('ngAnimate', ['ng']) * @param {string} className the CSS class that will be animated and then removed from the element * @return {Promise} the animation callback promise */ - removeClass : function(element, className) { - return this.setClass(element, [], className); + removeClass : function(element, className, options) { + return this.setClass(element, [], className, options); }, /** @@ -973,7 +985,9 @@ angular.module('ngAnimate', ['ng']) * CSS classes have been set on the element * @return {Promise} the animation callback promise */ - setClass : function(element, add, remove) { + setClass : function(element, add, remove, options) { + options = parseAnimateOptions(options); + var STORAGE_KEY = '$$animateClasses'; element = angular.element(element); element = stripCommentsFromElement(element); @@ -1007,11 +1021,16 @@ angular.module('ngAnimate', ['ng']) }); if (hasCache) { + if (options && cache.options) { + cache.options = cache.options.concat(options); + } + //the digest cycle will combine all the animations into one function return cache.promise; } else { element.data(STORAGE_KEY, cache = { - classes : classes + classes : classes, + options : options }); } @@ -1034,7 +1053,7 @@ angular.module('ngAnimate', ['ng']) ? done() : performAnimation('setClass', classes, element, parentElement, null, function() { $delegate.setClass(element, classes[0], classes[1]); - }, done); + }, cache.options, done); }); }, @@ -1096,7 +1115,7 @@ angular.module('ngAnimate', ['ng']) CSS code. Element, parentElement and afterElement are provided DOM elements for the animation and the onComplete callback will be fired once the animation is fully complete. */ - function performAnimation(animationEvent, className, element, parentElement, afterElement, domOperation, doneCallback) { + function performAnimation(animationEvent, className, element, parentElement, afterElement, domOperation, options, doneCallback) { var noopCancel = noop; var runner = animationRunner(element, animationEvent, className); @@ -1204,6 +1223,11 @@ angular.module('ngAnimate', ['ng']) //the ng-animate class does nothing, but it's here to allow for //parent animations to find and cancel child animations when needed element.addClass(NG_ANIMATE_CLASS_NAME); + if (isArray(options)) { + forEach(options, function(className) { + element.addClass(className); + }); + } var localAnimationCount = globalAnimationCounter++; totalActiveAnimations++; @@ -1273,8 +1297,15 @@ angular.module('ngAnimate', ['ng']) function closeAnimation() { if (!closeAnimation.hasBeenRun) { closeAnimation.hasBeenRun = true; + if (isArray(options)) { + forEach(options, function(className) { + element.removeClass(className); + }); + } + var data = element.data(NG_ANIMATE_STATE); if (data) { + /* only structural animations wait for reflow before removing an animation, but class-based animations don't. An example of this failing would be when a parent HTML tag has a ng-class attribute @@ -1539,7 +1570,7 @@ angular.module('ngAnimate', ['ng']) function parseMaxTime(str) { var maxValue = 0; - var values = angular.isString(str) ? + var values = isString(str) ? str.split(/\s*,\s*/) : []; forEach(values, function(value) { diff --git a/src/ngMock/angular-mocks.js b/src/ngMock/angular-mocks.js index 7d942204e56b..00e4700e2214 100644 --- a/src/ngMock/angular-mocks.js +++ b/src/ngMock/angular-mocks.js @@ -809,6 +809,7 @@ angular.mock.animate = angular.module('ngAnimateMock', ['ng']) animate.queue.push({ event : method, element : arguments[0], + options : arguments[arguments.length-1], args : arguments }); return $delegate[method].apply($delegate, arguments); diff --git a/test/ng/directive/ngShowHideSpec.js b/test/ng/directive/ngShowHideSpec.js index 7c73c0aa4195..5140df4fbef1 100644 --- a/test/ng/directive/ngShowHideSpec.js +++ b/test/ng/directive/ngShowHideSpec.js @@ -156,6 +156,28 @@ describe('ngShow / ngHide animations', function() { expect(item.element.text()).toBe('data'); expect(item.element).toBeHidden(); })); + + it('should apply the temporary `.ng-hide-animate` class to the element', + inject(function($compile, $rootScope, $animate) { + + var item; + var $scope = $rootScope.$new(); + $scope.on = false; + element = $compile(html( + '<div class="show-hide" ng-show="on">data</div>' + ))($scope); + $scope.$digest(); + + item = $animate.queue.shift(); + expect(item.event).toEqual('addClass'); + expect(item.options).toEqual('ng-hide-animate'); + + $scope.on = true; + $scope.$digest(); + item = $animate.queue.shift(); + expect(item.event).toEqual('removeClass'); + expect(item.options).toEqual('ng-hide-animate'); + })); }); describe('ngHide', function() { @@ -181,5 +203,27 @@ describe('ngShow / ngHide animations', function() { expect(item.element.text()).toBe('datum'); expect(item.element).toBeShown(); })); + + it('should apply the temporary `.ng-hide-animate` class to the element', + inject(function($compile, $rootScope, $animate) { + + var item; + var $scope = $rootScope.$new(); + $scope.on = false; + element = $compile(html( + '<div class="show-hide" ng-hide="on">data</div>' + ))($scope); + $scope.$digest(); + + item = $animate.queue.shift(); + expect(item.event).toEqual('removeClass'); + expect(item.options).toEqual('ng-hide-animate'); + + $scope.on = true; + $scope.$digest(); + item = $animate.queue.shift(); + expect(item.event).toEqual('addClass'); + expect(item.options).toEqual('ng-hide-animate'); + })); }); }); diff --git a/test/ngAnimate/animateSpec.js b/test/ngAnimate/animateSpec.js index e2440b343c1a..3815caf36a36 100644 --- a/test/ngAnimate/animateSpec.js +++ b/test/ngAnimate/animateSpec.js @@ -2450,6 +2450,78 @@ describe("ngAnimate", function() { })); }); + describe("options", function() { + + it('should add and remove the temporary className value is provided', function() { + var captures = {}; + module(function($animateProvider) { + $animateProvider.register('.capture', function() { + return { + enter : capture('enter'), + leave : capture('leave'), + move : capture('move'), + addClass : capture('addClass'), + removeClass : capture('removeClass'), + setClass : capture('setClass') + }; + + function capture(event) { + return function(element, add, remove, done) { + //some animations only have one extra param + done = done || remove || add; + captures[event]=done; + }; + } + }); + }); + inject(function($animate, $rootScope, $compile, $rootElement, $document) { + var container = jqLite('<div class="container"></div>'); + var container2 = jqLite('<div class="container2"></div>'); + var element = jqLite('<div class="capture"></div>'); + $rootElement.append(container); + $rootElement.append(container2); + angular.element($document[0].body).append($rootElement); + + $compile(element)($rootScope); + + assertTempClass('enter', 'temp-enter', function() { + $animate.enter(element, container, null, 'temp-enter'); + }); + + assertTempClass('move', 'temp-move', function() { + $animate.move(element, null, container2, 'temp-move'); + }); + + assertTempClass('addClass', 'temp-add', function() { + $animate.addClass(element, 'add', 'temp-add'); + }); + + assertTempClass('removeClass', 'temp-remove', function() { + $animate.removeClass(element, 'add', 'temp-remove'); + }); + + element.addClass('remove'); + assertTempClass('setClass', 'temp-set', function() { + $animate.setClass(element, 'add', 'remove', 'temp-set'); + }); + + assertTempClass('leave', 'temp-leave', function() { + $animate.leave(element, 'temp-leave'); + }); + + function assertTempClass(event, className, animationOperation) { + expect(element).not.toHaveClass(className); + animationOperation(); + $rootScope.$digest(); + expect(element).toHaveClass(className); + $animate.triggerReflow(); + captures[event](); + $animate.triggerCallbacks(); + expect(element).not.toHaveClass(className); + } + }); + }); + }); describe("addClass / removeClass", function() {