From 6744cbd56d87425dc4372e735d262f7742f1fe10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matias=20Niemel=C3=A4?= Date: Fri, 5 Sep 2014 15:48:58 -0400 Subject: [PATCH] fix($animate): abort class-based animations if the element is removed during digest Prior to this fix, if the element is removed before the digest kicks off then it leads to an error when a class based animation is run. This fix ensures that the animation will not run at all if the element does not have a parent element. Closes #8796 --- src/ngAnimate/animate.js | 16 +++++- test/ngAnimate/animateSpec.js | 95 +++++++++++++++++++++++++++++++++++ 2 files changed, 110 insertions(+), 1 deletion(-) diff --git a/src/ngAnimate/animate.js b/src/ngAnimate/animate.js index e66fcd6e1cfc..4178a1acb383 100644 --- a/src/ngAnimate/animate.js +++ b/src/ngAnimate/animate.js @@ -1015,6 +1015,20 @@ angular.module('ngAnimate', ['ng']) } return cache.promise = runAnimationPostDigest(function(done) { + var parentElement; + + //any ancestor elements past $rootElement's are not apart of the + //app therefore there is no need to examine the parent element + if (!isMatchingElement(element, $rootElement)) { + parentElement = element.parent(); + var elementNode = extractElementNode(element); + var parentNode = extractElementNode(parentElement); + if (!parentNode || parentNode['$$NG_REMOVED'] || elementNode['$$NG_REMOVED']) { + done(); + return; + } + } + var cache = element.data(STORAGE_KEY); element.removeData(STORAGE_KEY); @@ -1022,7 +1036,7 @@ angular.module('ngAnimate', ['ng']) var classes = resolveElementClasses(element, cache, state.active); return !classes ? done() - : performAnimation('setClass', classes, element, null, null, function() { + : performAnimation('setClass', classes, element, parentElement, null, function() { $delegate.setClass(element, classes[0], classes[1]); }, done); }); diff --git a/test/ngAnimate/animateSpec.js b/test/ngAnimate/animateSpec.js index a571464e2142..a494147acf2d 100644 --- a/test/ngAnimate/animateSpec.js +++ b/test/ngAnimate/animateSpec.js @@ -3413,6 +3413,101 @@ describe("ngAnimate", function() { }); }); + it('should skip class-based animations if the element is removed before the digest occurs', function() { + var spy = jasmine.createSpy(); + module(function($animateProvider) { + $animateProvider.register('.animated', function() { + return { + beforeAddClass : spy, + beforeRemoveClass : spy, + beforeSetClass : spy + }; + }); + }); + inject(function($rootScope, $animate, $compile, $rootElement, $document) { + $animate.enabled(true); + + var one = $compile('
')($rootScope); + var two = $compile('
')($rootScope); + var three = $compile('
')($rootScope); + + $rootElement.append(one); + $rootElement.append(two); + angular.element($document[0].body).append($rootElement); + + $animate.addClass(one, 'active-class'); + one.remove(); + + $rootScope.$digest(); + expect(spy).not.toHaveBeenCalled(); + + $animate.addClass(two, 'active-class'); + + $rootScope.$digest(); + expect(spy).toHaveBeenCalled(); + + spy.reset(); + $animate.removeClass(two, 'active-class'); + two.remove(); + + $rootScope.$digest(); + expect(spy).not.toHaveBeenCalled(); + + $animate.setClass(three, 'active-class', 'three'); + three.remove(); + + $rootScope.$digest(); + expect(spy).not.toHaveBeenCalled(); + }); + }); + + it('should skip class-based animations if ngRepeat has marked the element or its parent for removal', function() { + var spy = jasmine.createSpy(); + module(function($animateProvider) { + $animateProvider.register('.animated', function() { + return { + beforeAddClass : spy, + beforeRemoveClass : spy, + beforeSetClass : spy + }; + }); + }); + inject(function($rootScope, $animate, $compile, $rootElement, $document) { + $animate.enabled(true); + + var element = $compile( + '
' + + '
' + + ' {{ $index }}' + + '
' + + '
' + )($rootScope); + + $rootElement.append(element); + angular.element($document[0].body).append($rootElement); + + $rootScope.items = [1,2,3]; + $rootScope.$digest(); + + var child = element.find('div'); + + $animate.addClass(child, 'start-animation'); + $rootScope.items = [2,3]; + $rootScope.$digest(); + + expect(spy).not.toHaveBeenCalled(); + + var innerChild = element.find('span'); + + $animate.addClass(innerChild, 'start-animation'); + $rootScope.items = [3]; + $rootScope.$digest(); + + expect(spy).not.toHaveBeenCalled(); + dealoc(element); + }); + }); + it('should call class-based animation callbacks in the correct order when animations are skipped', function() { var continueAnimation; module(function($animateProvider) {