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( + '