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

Commit 16b82c6

Browse files
authored
fix($animate): let cancel() reject the runner promise
Closes #14204 Closes #16373 BREAKING CHANGE: $animate.cancel(runner) now rejects the underlying promise and calls the catch() handler on the runner returned by $animate functions (enter, leave, move, addClass, removeClass, setClass, animate). Previously it would resolve the promise as if the animation had ended successfully. Example: ```js var runner = $animate.addClass('red'); runner.then(function() { console.log('success')}); runner.catch(function() { console.log('cancelled')}); runner.cancel(); ``` Pre-1.7.0, this logs 'success', 1.7.0 and later it logs 'cancelled'. To migrate, add a catch() handler to your animation runners.
1 parent e3ece2f commit 16b82c6

File tree

2 files changed

+266
-12
lines changed

2 files changed

+266
-12
lines changed

src/ng/animate.js

+76-12
Original file line numberDiff line numberDiff line change
@@ -464,13 +464,77 @@ var $AnimateProvider = ['$provide', /** @this */ function($provide) {
464464
* @ngdoc method
465465
* @name $animate#cancel
466466
* @kind function
467-
* @description Cancels the provided animation.
468-
*
469-
* @param {Promise} animationPromise The animation promise that is returned when an animation is started.
467+
* @description Cancels the provided animation and applies the end state of the animation.
468+
* Note that this does not cancel the underlying operation, e.g. the setting of classes or
469+
* adding the element to the DOM.
470+
*
471+
* @param {animationRunner} animationRunner An animation runner returned by an $animate function.
472+
*
473+
* @example
474+
<example module="animationExample" deps="angular-animate.js" animations="true" name="animate-cancel">
475+
<file name="app.js">
476+
angular.module('animationExample', ['ngAnimate']).component('cancelExample', {
477+
templateUrl: 'template.html',
478+
controller: function($element, $animate) {
479+
this.runner = null;
480+
481+
this.addClass = function() {
482+
this.runner = $animate.addClass($element.find('div'), 'red');
483+
var ctrl = this;
484+
this.runner.finally(function() {
485+
ctrl.runner = null;
486+
});
487+
};
488+
489+
this.removeClass = function() {
490+
this.runner = $animate.removeClass($element.find('div'), 'red');
491+
var ctrl = this;
492+
this.runner.finally(function() {
493+
ctrl.runner = null;
494+
});
495+
};
496+
497+
this.cancel = function() {
498+
$animate.cancel(this.runner);
499+
};
500+
}
501+
});
502+
</file>
503+
<file name="template.html">
504+
<p>
505+
<button id="add" ng-click="$ctrl.addClass()">Add</button>
506+
<button ng-click="$ctrl.removeClass()">Remove</button>
507+
<br>
508+
<button id="cancel" ng-click="$ctrl.cancel()" ng-disabled="!$ctrl.runner">Cancel</button>
509+
<br>
510+
<div id="target">CSS-Animated Text</div>
511+
</p>
512+
</file>
513+
<file name="index.html">
514+
<cancel-example></cancel-example>
515+
</file>
516+
<file name="style.css">
517+
.red-add, .red-remove {
518+
transition: all 4s cubic-bezier(0.250, 0.460, 0.450, 0.940);
519+
}
520+
521+
.red,
522+
.red-add.red-add-active {
523+
color: #FF0000;
524+
font-size: 40px;
525+
}
526+
527+
.red-remove.red-remove-active {
528+
font-size: 10px;
529+
color: black;
530+
}
531+
532+
</file>
533+
</example>
470534
*/
471535
cancel: function(runner) {
472-
if (runner.end) {
473-
runner.end();
536+
if (runner.cancel) {
537+
runner.cancel();
474538
}
475539
},
476540

@@ -496,7 +560,7 @@ var $AnimateProvider = ['$provide', /** @this */ function($provide) {
496560
* - **removeClass** - `{string}` - space-separated CSS classes to remove from element
497561
* - **to** - `{Object}` - CSS properties & values at end of animation. Must have matching `from`
498562
*
499-
* @return {Promise} the animation callback promise
563+
* @return {Runner} the animation runner
500564
*/
501565
enter: function(element, parent, after, options) {
502566
parent = parent && jqLite(parent);
@@ -528,7 +592,7 @@ var $AnimateProvider = ['$provide', /** @this */ function($provide) {
528592
* - **removeClass** - `{string}` - space-separated CSS classes to remove from element
529593
* - **to** - `{Object}` - CSS properties & values at end of animation. Must have matching `from`
530594
*
531-
* @return {Promise} the animation callback promise
595+
* @return {Runner} the animation runner
532596
*/
533597
move: function(element, parent, after, options) {
534598
parent = parent && jqLite(parent);
@@ -555,7 +619,7 @@ var $AnimateProvider = ['$provide', /** @this */ function($provide) {
555619
* - **removeClass** - `{string}` - space-separated CSS classes to remove from element
556620
* - **to** - `{Object}` - CSS properties & values at end of animation. Must have matching `from`
557621
*
558-
* @return {Promise} the animation callback promise
622+
* @return {Runner} the animation runner
559623
*/
560624
leave: function(element, options) {
561625
return $$animateQueue.push(element, 'leave', prepareAnimateOptions(options), function() {
@@ -585,7 +649,7 @@ var $AnimateProvider = ['$provide', /** @this */ function($provide) {
585649
* - **removeClass** - `{string}` - space-separated CSS classes to remove from element
586650
* - **to** - `{Object}` - CSS properties & values at end of animation. Must have matching `from`
587651
*
588-
* @return {Promise} the animation callback promise
652+
* @return {Runner} animationRunner the animation runner
589653
*/
590654
addClass: function(element, className, options) {
591655
options = prepareAnimateOptions(options);
@@ -615,7 +679,7 @@ var $AnimateProvider = ['$provide', /** @this */ function($provide) {
615679
* - **removeClass** - `{string}` - space-separated CSS classes to remove from element
616680
* - **to** - `{Object}` - CSS properties & values at end of animation. Must have matching `from`
617681
*
618-
* @return {Promise} the animation callback promise
682+
* @return {Runner} the animation runner
619683
*/
620684
removeClass: function(element, className, options) {
621685
options = prepareAnimateOptions(options);
@@ -646,7 +710,7 @@ var $AnimateProvider = ['$provide', /** @this */ function($provide) {
646710
* - **removeClass** - `{string}` - space-separated CSS classes to remove from element
647711
* - **to** - `{Object}` - CSS properties & values at end of animation. Must have matching `from`
648712
*
649-
* @return {Promise} the animation callback promise
713+
* @return {Runner} the animation runner
650714
*/
651715
setClass: function(element, add, remove, options) {
652716
options = prepareAnimateOptions(options);
@@ -693,7 +757,7 @@ var $AnimateProvider = ['$provide', /** @this */ function($provide) {
693757
* - **removeClass** - `{string}` - space-separated CSS classes to remove from element
694758
* - **to** - `{Object}` - CSS properties & values at end of animation. Must have matching `from`
695759
*
696-
* @return {Promise} the animation callback promise
760+
* @return {Runner} the animation runner
697761
*/
698762
animate: function(element, from, to, className, options) {
699763
options = prepareAnimateOptions(options);

test/ngAnimate/animateSpec.js

+190
Original file line numberDiff line numberDiff line change
@@ -790,6 +790,7 @@ describe('animations', function() {
790790
expect(element).toHaveClass('red');
791791
}));
792792

793+
793794
it('removeClass() should issue a removeClass animation with the correct DOM operation', inject(function($animate, $rootScope) {
794795
parent.append(element);
795796
element.addClass('blue');
@@ -934,6 +935,195 @@ describe('animations', function() {
934935
}));
935936
});
936937

938+
939+
describe('$animate.cancel()', function() {
940+
941+
it('should cancel enter()', inject(function($animate, $rootScope) {
942+
expect(parent.children().length).toBe(0);
943+
944+
options.foo = 'bar';
945+
var spy = jasmine.createSpy('cancelCatch');
946+
947+
var runner = $animate.enter(element, parent, null, options);
948+
949+
runner.catch(spy);
950+
951+
expect(parent.children().length).toBe(1);
952+
953+
$rootScope.$digest();
954+
955+
expect(capturedAnimation[0]).toBe(element);
956+
expect(capturedAnimation[1]).toBe('enter');
957+
expect(capturedAnimation[2].foo).toEqual(options.foo);
958+
959+
$animate.cancel(runner);
960+
// Since enter() immediately adds the element, we can only check if the
961+
// element is still at the position
962+
expect(parent.children().length).toBe(1);
963+
964+
$rootScope.$digest();
965+
966+
// Catch handler is called after digest
967+
expect(spy).toHaveBeenCalled();
968+
}));
969+
970+
971+
it('should cancel move()', inject(function($animate, $rootScope) {
972+
parent.append(element);
973+
974+
expect(parent.children().length).toBe(1);
975+
expect(parent2.children().length).toBe(0);
976+
977+
options.foo = 'bar';
978+
var spy = jasmine.createSpy('cancelCatch');
979+
980+
var runner = $animate.move(element, parent2, null, options);
981+
runner.catch(spy);
982+
983+
expect(parent.children().length).toBe(0);
984+
expect(parent2.children().length).toBe(1);
985+
986+
$rootScope.$digest();
987+
988+
expect(capturedAnimation[0]).toBe(element);
989+
expect(capturedAnimation[1]).toBe('move');
990+
expect(capturedAnimation[2].foo).toEqual(options.foo);
991+
992+
$animate.cancel(runner);
993+
// Since moves() immediately moves the element, we can only check if the
994+
// element is still at the correct position
995+
expect(parent.children().length).toBe(0);
996+
expect(parent2.children().length).toBe(1);
997+
998+
$rootScope.$digest();
999+
1000+
// Catch handler is called after digest
1001+
expect(spy).toHaveBeenCalled();
1002+
}));
1003+
1004+
1005+
it('cancel leave()', inject(function($animate, $rootScope) {
1006+
parent.append(element);
1007+
options.foo = 'bar';
1008+
var spy = jasmine.createSpy('cancelCatch');
1009+
1010+
var runner = $animate.leave(element, options);
1011+
1012+
runner.catch(spy);
1013+
$rootScope.$digest();
1014+
1015+
expect(capturedAnimation[0]).toBe(element);
1016+
expect(capturedAnimation[1]).toBe('leave');
1017+
expect(capturedAnimation[2].foo).toEqual(options.foo);
1018+
1019+
expect(element.parent().length).toBe(1);
1020+
1021+
$animate.cancel(runner);
1022+
// Animation concludes immediately
1023+
expect(element.parent().length).toBe(0);
1024+
expect(spy).not.toHaveBeenCalled();
1025+
1026+
$rootScope.$digest();
1027+
// Catch handler is called after digest
1028+
expect(spy).toHaveBeenCalled();
1029+
}));
1030+
1031+
it('should cancel addClass()', inject(function($animate, $rootScope) {
1032+
parent.append(element);
1033+
options.foo = 'bar';
1034+
var runner = $animate.addClass(element, 'red', options);
1035+
var spy = jasmine.createSpy('cancelCatch');
1036+
1037+
runner.catch(spy);
1038+
$rootScope.$digest();
1039+
1040+
expect(capturedAnimation[0]).toBe(element);
1041+
expect(capturedAnimation[1]).toBe('addClass');
1042+
expect(capturedAnimation[2].foo).toEqual(options.foo);
1043+
1044+
$animate.cancel(runner);
1045+
expect(element).toHaveClass('red');
1046+
expect(spy).not.toHaveBeenCalled();
1047+
1048+
$rootScope.$digest();
1049+
expect(spy).toHaveBeenCalled();
1050+
}));
1051+
1052+
1053+
it('should cancel setClass()', inject(function($animate, $rootScope) {
1054+
parent.append(element);
1055+
element.addClass('red');
1056+
options.foo = 'bar';
1057+
1058+
var runner = $animate.setClass(element, 'blue', 'red', options);
1059+
var spy = jasmine.createSpy('cancelCatch');
1060+
1061+
runner.catch(spy);
1062+
$rootScope.$digest();
1063+
1064+
expect(capturedAnimation[0]).toBe(element);
1065+
expect(capturedAnimation[1]).toBe('setClass');
1066+
expect(capturedAnimation[2].foo).toEqual(options.foo);
1067+
1068+
$animate.cancel(runner);
1069+
expect(element).toHaveClass('blue');
1070+
expect(element).not.toHaveClass('red');
1071+
expect(spy).not.toHaveBeenCalled();
1072+
1073+
$rootScope.$digest();
1074+
expect(spy).toHaveBeenCalled();
1075+
}));
1076+
1077+
1078+
it('should cancel removeClass()', inject(function($animate, $rootScope) {
1079+
parent.append(element);
1080+
element.addClass('red blue');
1081+
1082+
options.foo = 'bar';
1083+
var runner = $animate.removeClass(element, 'red', options);
1084+
var spy = jasmine.createSpy('cancelCatch');
1085+
1086+
runner.catch(spy);
1087+
$rootScope.$digest();
1088+
1089+
expect(capturedAnimation[0]).toBe(element);
1090+
expect(capturedAnimation[1]).toBe('removeClass');
1091+
expect(capturedAnimation[2].foo).toEqual(options.foo);
1092+
1093+
$animate.cancel(runner);
1094+
expect(element).not.toHaveClass('red');
1095+
expect(element).toHaveClass('blue');
1096+
1097+
$rootScope.$digest();
1098+
expect(spy).toHaveBeenCalled();
1099+
}));
1100+
1101+
1102+
it('should cancel animate()',
1103+
inject(function($animate, $rootScope) {
1104+
1105+
parent.append(element);
1106+
1107+
var fromStyle = { color: 'blue' };
1108+
var options = { addClass: 'red' };
1109+
1110+
var runner = $animate.animate(element, fromStyle, null, null, options);
1111+
var spy = jasmine.createSpy('cancelCatch');
1112+
1113+
runner.catch(spy);
1114+
$rootScope.$digest();
1115+
1116+
expect(capturedAnimation).toBeTruthy();
1117+
1118+
$animate.cancel(runner);
1119+
expect(element).toHaveClass('red');
1120+
1121+
$rootScope.$digest();
1122+
expect(spy).toHaveBeenCalled();
1123+
}));
1124+
});
1125+
1126+
9371127
describe('parent animations', function() {
9381128
they('should not cancel a pre-digest parent class-based animation if a child $prop animation is set to run',
9391129
['structural', 'class-based'], function(animationType) {

0 commit comments

Comments
 (0)