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

Commit c12c02f

Browse files
committed
fix($animate): cancel any ongoing child animations during move and leave animations
1 parent 055fd4c commit c12c02f

File tree

2 files changed

+110
-12
lines changed

2 files changed

+110
-12
lines changed

src/ngAnimate/animate.js

+31-7
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,7 @@ angular.module('ngAnimate', ['ng'])
200200
var selectors = $animateProvider.$$selectors;
201201

202202
var NG_ANIMATE_STATE = '$$ngAnimateState';
203+
var NG_ANIMATE_CLASS_NAME = 'ng-animate';
203204
var rootAnimateState = {running:true};
204205
$provide.decorator('$animate', ['$delegate', '$injector', '$sniffer', '$rootElement', '$timeout', '$rootScope',
205206
function($delegate, $injector, $sniffer, $rootElement, $timeout, $rootScope) {
@@ -320,6 +321,7 @@ angular.module('ngAnimate', ['ng'])
320321
* @param {function()=} done callback function that will be called once the animation is complete
321322
*/
322323
leave : function(element, done) {
324+
cancelChildAnimations(element);
323325
$rootScope.$$postDigest(function() {
324326
performAnimation('leave', 'ng-leave', element, null, null, function() {
325327
$delegate.leave(element, done);
@@ -358,6 +360,7 @@ angular.module('ngAnimate', ['ng'])
358360
* @param {function()=} done callback function that will be called once the animation is complete
359361
*/
360362
move : function(element, parent, after, done) {
363+
cancelChildAnimations(element);
361364
$delegate.move(element, parent, after);
362365
$rootScope.$$postDigest(function() {
363366
performAnimation('move', 'ng-move', element, null, null, function() {
@@ -503,6 +506,10 @@ angular.module('ngAnimate', ['ng'])
503506
done:done
504507
});
505508

509+
//the ng-animate class does nothing, but it's here to allow for
510+
//parent animations to find and cancel child animations when needed
511+
element.addClass(NG_ANIMATE_CLASS_NAME);
512+
506513
forEach(animations, function(animation, index) {
507514
var fn = function() {
508515
progress(index);
@@ -519,12 +526,6 @@ angular.module('ngAnimate', ['ng'])
519526
}
520527
});
521528

522-
function cancelAnimations(animations) {
523-
var isCancelledFlag = true;
524-
forEach(animations, function(animation) {
525-
(animation.endFn || noop)(isCancelledFlag);
526-
});
527-
}
528529

529530
function progress(index) {
530531
animations[index].done = true;
@@ -538,11 +539,34 @@ angular.module('ngAnimate', ['ng'])
538539
function done() {
539540
if(!done.hasBeenRun) {
540541
done.hasBeenRun = true;
541-
element.removeData(NG_ANIMATE_STATE);
542+
cleanup(element);
542543
(onComplete || noop)();
543544
}
544545
}
545546
}
547+
548+
function cancelChildAnimations(element) {
549+
angular.forEach(element[0].querySelectorAll('.' + NG_ANIMATE_CLASS_NAME), function(element) {
550+
element = angular.element(element);
551+
var data = element.data(NG_ANIMATE_STATE);
552+
if(data) {
553+
cancelAnimations(data.animations);
554+
cleanup(element);
555+
}
556+
});
557+
}
558+
559+
function cancelAnimations(animations) {
560+
var isCancelledFlag = true;
561+
forEach(animations, function(animation) {
562+
(animation.endFn || noop)(isCancelledFlag);
563+
});
564+
}
565+
566+
function cleanup(element) {
567+
element.removeClass(NG_ANIMATE_CLASS_NAME);
568+
element.removeData(NG_ANIMATE_STATE);
569+
}
546570
}]);
547571

548572
$animateProvider.register('', ['$window', '$sniffer', function($window, $sniffer) {

test/ngAnimate/animateSpec.js

+79-5
Original file line numberDiff line numberDiff line change
@@ -696,8 +696,9 @@ describe("ngAnimate", function() {
696696
$rootScope.$digest();
697697

698698
if ($sniffer.transitions) {
699-
expect(element.hasClass('abc ng-enter')).toBe(true);
700-
expect(element.hasClass('abc ng-enter ng-enter-active')).toBe(true);
699+
expect(element.hasClass('abc')).toBe(true);
700+
expect(element.hasClass('ng-enter')).toBe(true);
701+
expect(element.hasClass('ng-enter-active')).toBe(true);
701702
browserTrigger(element,'transitionend', { timeStamp: Date.now() + 22000, elapsedTime: 22000 });
702703
}
703704
expect(element.hasClass('abc')).toBe(true);
@@ -708,7 +709,8 @@ describe("ngAnimate", function() {
708709

709710
if ($sniffer.transitions) {
710711
expect(element.hasClass('xyz')).toBe(true);
711-
expect(element.hasClass('xyz ng-enter ng-enter-active')).toBe(true);
712+
expect(element.hasClass('ng-enter')).toBe(true);
713+
expect(element.hasClass('ng-enter-active')).toBe(true);
712714
browserTrigger(element,'transitionend', { timeStamp: Date.now() + 11000, elapsedTime: 11000 });
713715
}
714716
expect(element.hasClass('xyz')).toBe(true);
@@ -732,8 +734,10 @@ describe("ngAnimate", function() {
732734
$animate.enter(element, parent);
733735
$rootScope.$digest();
734736
if($sniffer.transitions) {
735-
expect(element.hasClass('one two ng-enter')).toBe(true);
736-
expect(element.hasClass('one two ng-enter ng-enter-active')).toBe(true);
737+
expect(element.hasClass('one')).toBe(true);
738+
expect(element.hasClass('two')).toBe(true);
739+
expect(element.hasClass('ng-enter')).toBe(true);
740+
expect(element.hasClass('ng-enter-active')).toBe(true);
737741
expect(element.hasClass('one-active')).toBe(false);
738742
expect(element.hasClass('two-active')).toBe(false);
739743
browserTrigger(element,'transitionend', { timeStamp: Date.now() + 3000, elapsedTime: 3000 });
@@ -1574,4 +1578,74 @@ describe("ngAnimate", function() {
15741578
expect(element.contents().length).toBe(1);
15751579
}));
15761580

1581+
it("should cancel all child animations when a leave or move animation is triggered on a parent element", function() {
1582+
1583+
var animationState;
1584+
module(function($animateProvider) {
1585+
$animateProvider.register('.animan', function($timeout) {
1586+
return {
1587+
enter : function(element, done) {
1588+
animationState = 'enter';
1589+
$timeout(done, 0, false);
1590+
return function() {
1591+
animationState = 'enter-cancel';
1592+
}
1593+
},
1594+
addClass : function(element, className, done) {
1595+
animationState = 'addClass';
1596+
$timeout(done, 0, false);
1597+
return function() {
1598+
animationState = 'addClass-cancel';
1599+
}
1600+
}
1601+
};
1602+
});
1603+
});
1604+
1605+
inject(function($animate, $compile, $rootScope, $timeout, $sniffer) {
1606+
var element = html($compile('<div class="parent"></div>')($rootScope));
1607+
var container = html($compile('<div class="container"></div>')($rootScope));
1608+
var child = html($compile('<div class="animan child"></div>')($rootScope));
1609+
1610+
ss.addRule('.animan.ng-enter, .animan.something-add', '-webkit-transition: width 1s, background 1s 1s;' +
1611+
'transition: width 1s, background 1s 1s;');
1612+
1613+
$rootElement.append(element);
1614+
jqLite(document.body).append($rootElement);
1615+
1616+
$animate.enter(child, element);
1617+
$rootScope.$digest();
1618+
1619+
expect(animationState).toBe('enter');
1620+
if($sniffer.transitions) {
1621+
expect(child.hasClass('ng-enter')).toBe(true);
1622+
expect(child.hasClass('ng-enter-active')).toBe(true);
1623+
}
1624+
1625+
$animate.move(element, container);
1626+
if($sniffer.transitions) {
1627+
expect(child.hasClass('ng-enter')).toBe(false);
1628+
expect(child.hasClass('ng-enter-active')).toBe(false);
1629+
}
1630+
1631+
expect(animationState).toBe('enter-cancel');
1632+
$rootScope.$digest();
1633+
$timeout.flush();
1634+
1635+
$animate.addClass(child, 'something');
1636+
expect(animationState).toBe('addClass');
1637+
if($sniffer.transitions) {
1638+
expect(child.hasClass('something-add')).toBe(true);
1639+
expect(child.hasClass('something-add-active')).toBe(true);
1640+
}
1641+
1642+
$animate.leave(container);
1643+
expect(animationState).toBe('addClass-cancel');
1644+
if($sniffer.transitions) {
1645+
expect(child.hasClass('something-add')).toBe(false);
1646+
expect(child.hasClass('something-add-active')).toBe(false);
1647+
}
1648+
});
1649+
});
1650+
15771651
});

0 commit comments

Comments
 (0)