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

Commit eb63c53

Browse files
committed
fix(ngAnimate): let $animate.cancel() reject the runner promise
1 parent 41d5c90 commit eb63c53

File tree

2 files changed

+315
-12
lines changed

2 files changed

+315
-12
lines changed

src/ng/animate.js

Lines changed: 125 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -464,13 +464,126 @@ 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+
<file name="protractor.js" type="protractor">
534+
535+
it('should cancel the animation', function() {
536+
//Enable animations (we disable them by default, see protractor-shared-conf.js)
537+
element(by.css('body')).allowAnimations(true);
538+
// CSS transitions use $timeout, for which Protractor by default to finish,
539+
// before executing the next action.
540+
// In this case, this would mean that the click on "Cancel" is only executed
541+
// after the transition has finished
542+
browser.waitForAngularEnabled(false);
543+
544+
var target = element(by.id('target'));
545+
546+
target.getSize().then(function(size) {
547+
expect(size.height).toBeGreaterThan(17);
548+
expect(size.height).toBeLessThan(21);
549+
});
550+
551+
element(by.buttonText('Add')).click();
552+
553+
target.getSize().then(function(size) {
554+
expect(size.height).toBeGreaterThan(18);
555+
expect(size.height).toBeLessThan(25);
556+
});
557+
558+
var classes = target.getAttribute('class');
559+
560+
expect(classes).toContain('ng-animate');
561+
expect(classes).toContain('red-add-active');
562+
expect(classes).toContain('red');
563+
564+
element(by.buttonText('Cancel')).click();
565+
566+
target.getSize().then(function(size) {
567+
expect(size.height).toBeGreaterThan(45);
568+
expect(size.height).toBeLessThan(49);
569+
});
570+
571+
classes = target.getAttribute('class');
572+
573+
expect(classes).not.toContain('ng-animate');
574+
expect(classes).not.toContain('red-add-active');
575+
expect(classes).toContain('red');
576+
577+
// Re-enable waiting for AngularJS, and disable animations again
578+
browser.waitForAngularEnabled(true);
579+
element(by.css('body')).allowAnimations(false);
580+
});
581+
</file>
582+
</example>
470583
*/
471584
cancel: function(runner) {
472-
if (runner.end) {
473-
runner.end();
585+
if (runner.cancel) {
586+
runner.cancel();
474587
}
475588
},
476589

@@ -496,7 +609,7 @@ var $AnimateProvider = ['$provide', /** @this */ function($provide) {
496609
* - **removeClass** - `{string}` - space-separated CSS classes to remove from element
497610
* - **to** - `{Object}` - CSS properties & values at end of animation. Must have matching `from`
498611
*
499-
* @return {Promise} the animation callback promise
612+
* @return {Runner} the animation runner
500613
*/
501614
enter: function(element, parent, after, options) {
502615
parent = parent && jqLite(parent);
@@ -528,7 +641,7 @@ var $AnimateProvider = ['$provide', /** @this */ function($provide) {
528641
* - **removeClass** - `{string}` - space-separated CSS classes to remove from element
529642
* - **to** - `{Object}` - CSS properties & values at end of animation. Must have matching `from`
530643
*
531-
* @return {Promise} the animation callback promise
644+
* @return {Runner} the animation runner
532645
*/
533646
move: function(element, parent, after, options) {
534647
parent = parent && jqLite(parent);
@@ -555,7 +668,7 @@ var $AnimateProvider = ['$provide', /** @this */ function($provide) {
555668
* - **removeClass** - `{string}` - space-separated CSS classes to remove from element
556669
* - **to** - `{Object}` - CSS properties & values at end of animation. Must have matching `from`
557670
*
558-
* @return {Promise} the animation callback promise
671+
* @return {Runner} the animation runner
559672
*/
560673
leave: function(element, options) {
561674
return $$animateQueue.push(element, 'leave', prepareAnimateOptions(options), function() {
@@ -585,7 +698,7 @@ var $AnimateProvider = ['$provide', /** @this */ function($provide) {
585698
* - **removeClass** - `{string}` - space-separated CSS classes to remove from element
586699
* - **to** - `{Object}` - CSS properties & values at end of animation. Must have matching `from`
587700
*
588-
* @return {Promise} the animation callback promise
701+
* @return {Runner} animationRunner the animation runner
589702
*/
590703
addClass: function(element, className, options) {
591704
options = prepareAnimateOptions(options);
@@ -615,7 +728,7 @@ var $AnimateProvider = ['$provide', /** @this */ function($provide) {
615728
* - **removeClass** - `{string}` - space-separated CSS classes to remove from element
616729
* - **to** - `{Object}` - CSS properties & values at end of animation. Must have matching `from`
617730
*
618-
* @return {Promise} the animation callback promise
731+
* @return {Runner} the animation runner
619732
*/
620733
removeClass: function(element, className, options) {
621734
options = prepareAnimateOptions(options);
@@ -646,7 +759,7 @@ var $AnimateProvider = ['$provide', /** @this */ function($provide) {
646759
* - **removeClass** - `{string}` - space-separated CSS classes to remove from element
647760
* - **to** - `{Object}` - CSS properties & values at end of animation. Must have matching `from`
648761
*
649-
* @return {Promise} the animation callback promise
762+
* @return {Runner} the animation runner
650763
*/
651764
setClass: function(element, add, remove, options) {
652765
options = prepareAnimateOptions(options);
@@ -693,7 +806,7 @@ var $AnimateProvider = ['$provide', /** @this */ function($provide) {
693806
* - **removeClass** - `{string}` - space-separated CSS classes to remove from element
694807
* - **to** - `{Object}` - CSS properties & values at end of animation. Must have matching `from`
695808
*
696-
* @return {Promise} the animation callback promise
809+
* @return {Runner} the animation runner
697810
*/
698811
animate: function(element, from, to, className, options) {
699812
options = prepareAnimateOptions(options);

test/ngAnimate/animateSpec.js

Lines changed: 190 additions & 0 deletions
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)