Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.

Commit e988199

Browse files
committed
fix($animate): ensure that animateable directives cancel expired leave animations
If enter -> leave -> enter -> leave occurs then the first leave animation will animate alongside the second. This causes the very first DOM node (the view in ngView for example) to animate at the same time as the most recent DOM node which ends up being an undesired effect. This fix takes care of this issue. Closes #5886
1 parent c9245cf commit e988199

File tree

10 files changed

+269
-13
lines changed

10 files changed

+269
-13
lines changed

src/ng/directive/ngIf.js

+11-6
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ var ngIfDirective = ['$animate', function($animate) {
8484
restrict: 'A',
8585
$$tlb: true,
8686
link: function ($scope, $element, $attr, ctrl, $transclude) {
87-
var block, childScope;
87+
var block, childScope, previousElements;
8888
$scope.$watch($attr.ngIf, function ngIfWatchAction(value) {
8989

9090
if (toBoolean(value)) {
@@ -102,14 +102,19 @@ var ngIfDirective = ['$animate', function($animate) {
102102
});
103103
}
104104
} else {
105-
106-
if (childScope) {
105+
if(previousElements) {
106+
previousElements.remove();
107+
previousElements = null;
108+
}
109+
if(childScope) {
107110
childScope.$destroy();
108111
childScope = null;
109112
}
110-
111-
if (block) {
112-
$animate.leave(getBlockElements(block.clone));
113+
if(block) {
114+
previousElements = getBlockElements(block.clone);
115+
$animate.leave(previousElements, function() {
116+
previousElements = null;
117+
});
113118
block = null;
114119
}
115120
}

src/ng/directive/ngInclude.js

+10-2
Original file line numberDiff line numberDiff line change
@@ -177,15 +177,23 @@ var ngIncludeDirective = ['$http', '$templateCache', '$anchorScroll', '$animate'
177177
return function(scope, $element, $attr, ctrl, $transclude) {
178178
var changeCounter = 0,
179179
currentScope,
180+
previousElement,
180181
currentElement;
181182

182183
var cleanupLastIncludeContent = function() {
183-
if (currentScope) {
184+
if(previousElement) {
185+
previousElement.remove();
186+
previousElement = null;
187+
}
188+
if(currentScope) {
184189
currentScope.$destroy();
185190
currentScope = null;
186191
}
187192
if(currentElement) {
188-
$animate.leave(currentElement);
193+
$animate.leave(currentElement, function() {
194+
previousElement = null;
195+
});
196+
previousElement = currentElement;
189197
currentElement = null;
190198
}
191199
};

src/ng/directive/ngSwitch.js

+22-3
Original file line numberDiff line numberDiff line change
@@ -138,12 +138,31 @@ var ngSwitchDirective = ['$animate', function($animate) {
138138
var watchExpr = attr.ngSwitch || attr.on,
139139
selectedTranscludes,
140140
selectedElements,
141+
previousElements,
141142
selectedScopes = [];
142143

143144
scope.$watch(watchExpr, function ngSwitchWatchAction(value) {
144-
for (var i= 0, ii=selectedScopes.length; i<ii; i++) {
145-
selectedScopes[i].$destroy();
146-
$animate.leave(selectedElements[i]);
145+
var i, ii = selectedScopes.length;
146+
if(ii > 0) {
147+
if(previousElements) {
148+
for (i = 0; i < ii; i++) {
149+
previousElements[i].remove();
150+
}
151+
previousElements = null;
152+
}
153+
154+
previousElements = [];
155+
for (i= 0; i<ii; i++) {
156+
var selected = selectedElements[i];
157+
selectedScopes[i].$destroy();
158+
previousElements[i] = selected;
159+
$animate.leave(selected, function() {
160+
previousElements.splice(i, 1);
161+
if(previousElements.length === 0) {
162+
previousElements = null;
163+
}
164+
});
165+
}
147166
}
148167

149168
selectedElements = [];

src/ngAnimate/animate.js

+21
Original file line numberDiff line numberDiff line change
@@ -764,6 +764,27 @@ angular.module('ngAnimate', ['ng'])
764764
return;
765765
}
766766

767+
if(animationEvent == 'leave') {
768+
//there's no need to ever remove the listener since the element
769+
//will be removed (destroyed) after the leave animation ends or
770+
//is cancelled midway
771+
element.one('$destroy', function(e) {
772+
var element = angular.element(this);
773+
var state = element.data(NG_ANIMATE_STATE) || {};
774+
var activeLeaveAnimation = state.active['ng-leave'];
775+
if(activeLeaveAnimation) {
776+
var animations = activeLeaveAnimation.animations;
777+
778+
//if the before animation is completed then the element will be
779+
//removed shortly after so there is no need to cancel the animation
780+
if(!animations[0].beforeComplete) {
781+
cancelAnimations(animations);
782+
cleanup(element, 'ng-leave');
783+
}
784+
}
785+
});
786+
}
787+
767788
//the ng-animate class does nothing, but it's here to allow for
768789
//parent animations to find and cancel child animations when needed
769790
element.addClass(NG_ANIMATE_CLASS_NAME);

src/ngRoute/directive/ngView.js

+10-2
Original file line numberDiff line numberDiff line change
@@ -189,19 +189,27 @@ function ngViewFactory( $route, $anchorScroll, $animate) {
189189
link: function(scope, $element, attr, ctrl, $transclude) {
190190
var currentScope,
191191
currentElement,
192+
previousElement,
192193
autoScrollExp = attr.autoscroll,
193194
onloadExp = attr.onload || '';
194195

195196
scope.$on('$routeChangeSuccess', update);
196197
update();
197198

198199
function cleanupLastView() {
199-
if (currentScope) {
200+
if(previousElement) {
201+
previousElement.remove();
202+
previousElement = null;
203+
}
204+
if(currentScope) {
200205
currentScope.$destroy();
201206
currentScope = null;
202207
}
203208
if(currentElement) {
204-
$animate.leave(currentElement);
209+
$animate.leave(currentElement, function() {
210+
previousElement = null;
211+
});
212+
previousElement = currentElement;
205213
currentElement = null;
206214
}
207215
}

test/ng/directive/ngIfSpec.js

+38
Original file line numberDiff line numberDiff line change
@@ -277,4 +277,42 @@ describe('ngIf animations', function () {
277277
expect(element.children().length).toBe(0);
278278
}));
279279

280+
it('should destroy the previous leave animation if a new one takes place', function() {
281+
module(function($provide) {
282+
$provide.value('$animate', {
283+
enabled : function() { return true; },
284+
leave : function() {
285+
//DOM operation left blank
286+
},
287+
enter : function(element, parent) {
288+
parent.append(element);
289+
}
290+
});
291+
});
292+
inject(function ($compile, $rootScope, $animate) {
293+
var item;
294+
var $scope = $rootScope.$new();
295+
element = $compile(html(
296+
'<div>' +
297+
'<div ng-if="value">Yo</div>' +
298+
'</div>'
299+
))($scope);
300+
301+
$scope.$apply('value = true');
302+
303+
var destroyed, inner = element.children(0);
304+
inner.on('$destroy', function() {
305+
destroyed = true;
306+
});
307+
308+
$scope.$apply('value = false');
309+
310+
$scope.$apply('value = true');
311+
312+
$scope.$apply('value = false');
313+
314+
expect(destroyed).toBe(true);
315+
});
316+
});
317+
280318
});

test/ng/directive/ngIncludeSpec.js

+40
Original file line numberDiff line numberDiff line change
@@ -663,4 +663,44 @@ describe('ngInclude animations', function() {
663663
expect(itemA).not.toEqual(itemB);
664664
}));
665665

666+
it('should destroy the previous leave animation if a new one takes place', function() {
667+
module(function($provide) {
668+
$provide.value('$animate', {
669+
enabled : function() { return true; },
670+
leave : function() {
671+
//DOM operation left blank
672+
},
673+
enter : function(element, parent, after) {
674+
angular.element(after).after(element);
675+
}
676+
});
677+
});
678+
inject(function ($compile, $rootScope, $animate, $templateCache) {
679+
var item;
680+
var $scope = $rootScope.$new();
681+
element = $compile(html(
682+
'<div>' +
683+
'<div ng-include="inc">Yo</div>' +
684+
'</div>'
685+
))($scope);
686+
687+
$templateCache.put('one', [200, '<div>one</div>', {}]);
688+
$templateCache.put('two', [200, '<div>two</div>', {}]);
689+
690+
$scope.$apply('inc = "one"');
691+
692+
var destroyed, inner = element.children(0);
693+
inner.on('$destroy', function() {
694+
destroyed = true;
695+
});
696+
697+
$scope.$apply('inc = "two"');
698+
699+
$scope.$apply('inc = "one"');
700+
701+
$scope.$apply('inc = "two"');
702+
703+
expect(destroyed).toBe(true);
704+
});
705+
});
666706
});

test/ng/directive/ngSwitchSpec.js

+38
Original file line numberDiff line numberDiff line change
@@ -293,4 +293,42 @@ describe('ngSwitch animations', function() {
293293
expect(item.element.text()).toBe('three');
294294
}));
295295

296+
it('should destroy the previous leave animation if a new one takes place', function() {
297+
module(function($provide) {
298+
$provide.value('$animate', {
299+
enabled : function() { return true; },
300+
leave : function() {
301+
//DOM operation left blank
302+
},
303+
enter : function(element, parent, after) {
304+
angular.element(after).after(element);
305+
}
306+
});
307+
});
308+
inject(function ($compile, $rootScope, $animate, $templateCache) {
309+
var item;
310+
var $scope = $rootScope.$new();
311+
element = $compile(html(
312+
'<div ng-switch="inc">' +
313+
'<div ng-switch-when="one">one</div>' +
314+
'<div ng-switch-when="two">two</div>' +
315+
'</div>'
316+
))($scope);
317+
318+
$scope.$apply('inc = "one"');
319+
320+
var destroyed, inner = element.children(0);
321+
inner.on('$destroy', function() {
322+
destroyed = true;
323+
});
324+
325+
$scope.$apply('inc = "two"');
326+
327+
$scope.$apply('inc = "one"');
328+
329+
$scope.$apply('inc = "two"');
330+
331+
expect(destroyed).toBe(true);
332+
});
333+
});
296334
});

test/ngAnimate/animateSpec.js

+35
Original file line numberDiff line numberDiff line change
@@ -3335,5 +3335,40 @@ describe("ngAnimate", function() {
33353335
expect(cancelReflowCallback).toHaveBeenCalled();
33363336
});
33373337
});
3338+
3339+
it('should immediately close off a leave animation if the element is removed from the DOM', function() {
3340+
var stat;
3341+
module(function($animateProvider) {
3342+
$animateProvider.register('.going', function() {
3343+
return {
3344+
leave : function() {
3345+
//left blank so it hangs
3346+
stat = 'leaving';
3347+
return function(cancelled) {
3348+
stat = cancelled && 'gone';
3349+
};
3350+
}
3351+
};
3352+
});
3353+
});
3354+
inject(function($sniffer, $compile, $rootScope, $rootElement, $animate, $timeout) {
3355+
3356+
$animate.enabled(true);
3357+
3358+
var element = $compile('<div id="parentGuy"></div>')($rootScope);
3359+
var child = $compile('<div class="going"></div>')($rootScope);
3360+
$rootElement.append(element);
3361+
element.append(child);
3362+
3363+
$animate.leave(child);
3364+
$rootScope.$digest();
3365+
3366+
expect(stat).toBe('leaving');
3367+
3368+
child.remove();
3369+
3370+
expect(stat).toBe('gone');
3371+
});
3372+
});
33383373
});
33393374
});

test/ngRoute/directive/ngViewSpec.js

+44
Original file line numberDiff line numberDiff line change
@@ -833,6 +833,50 @@ describe('ngView animations', function() {
833833
}
834834
});
835835
});
836+
837+
it('should destroy the previous leave animation if a new one takes place', function() {
838+
module(function($provide) {
839+
$provide.value('$animate', {
840+
enabled : function() { return true; },
841+
leave : function() {
842+
//DOM operation left blank
843+
},
844+
enter : function(element, parent, after) {
845+
angular.element(after).after(element);
846+
}
847+
});
848+
});
849+
inject(function ($compile, $rootScope, $animate, $location) {
850+
var item;
851+
var $scope = $rootScope.$new();
852+
element = $compile(html(
853+
'<div>' +
854+
'<div ng-view></div>' +
855+
'</div>'
856+
))($scope);
857+
858+
$scope.$apply('value = true');
859+
860+
$location.path('/bar');
861+
$rootScope.$digest();
862+
863+
var destroyed, inner = element.children(0);
864+
inner.on('$destroy', function() {
865+
destroyed = true;
866+
});
867+
868+
$location.path('/foo');
869+
$rootScope.$digest();
870+
871+
$location.path('/bar');
872+
$rootScope.$digest();
873+
874+
$location.path('/bar');
875+
$rootScope.$digest();
876+
877+
expect(destroyed).toBe(true);
878+
});
879+
});
836880
});
837881

838882

0 commit comments

Comments
 (0)