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

Commit

Permalink
fix($animate): let cancel() reject the runner promise
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
Narretz authored Feb 2, 2018
1 parent e3ece2f commit 16b82c6
Show file tree
Hide file tree
Showing 2 changed files with 266 additions and 12 deletions.
88 changes: 76 additions & 12 deletions src/ng/animate.js
Original file line number Diff line number Diff line change
Expand Up @@ -464,13 +464,77 @@ var $AnimateProvider = ['$provide', /** @this */ function($provide) {
* @ngdoc method
* @name $animate#cancel
* @kind function
* @description Cancels the provided animation.
*
* @param {Promise} animationPromise The animation promise that is returned when an animation is started.
* @description Cancels the provided animation and applies the end state of the animation.
* Note that this does not cancel the underlying operation, e.g. the setting of classes or
* adding the element to the DOM.
*
* @param {animationRunner} animationRunner An animation runner returned by an $animate function.
*
* @example
<example module="animationExample" deps="angular-animate.js" animations="true" name="animate-cancel">
<file name="app.js">
angular.module('animationExample', ['ngAnimate']).component('cancelExample', {
templateUrl: 'template.html',
controller: function($element, $animate) {
this.runner = null;
this.addClass = function() {
this.runner = $animate.addClass($element.find('div'), 'red');
var ctrl = this;
this.runner.finally(function() {
ctrl.runner = null;
});
};
this.removeClass = function() {
this.runner = $animate.removeClass($element.find('div'), 'red');
var ctrl = this;
this.runner.finally(function() {
ctrl.runner = null;
});
};
this.cancel = function() {
$animate.cancel(this.runner);
};
}
});
</file>
<file name="template.html">
<p>
<button id="add" ng-click="$ctrl.addClass()">Add</button>
<button ng-click="$ctrl.removeClass()">Remove</button>
<br>
<button id="cancel" ng-click="$ctrl.cancel()" ng-disabled="!$ctrl.runner">Cancel</button>
<br>
<div id="target">CSS-Animated Text</div>
</p>
</file>
<file name="index.html">
<cancel-example></cancel-example>
</file>
<file name="style.css">
.red-add, .red-remove {
transition: all 4s cubic-bezier(0.250, 0.460, 0.450, 0.940);
}
.red,
.red-add.red-add-active {
color: #FF0000;
font-size: 40px;
}
.red-remove.red-remove-active {
font-size: 10px;
color: black;
}
</file>
</example>
*/
cancel: function(runner) {
if (runner.end) {
runner.end();
if (runner.cancel) {
runner.cancel();
}
},

Expand All @@ -496,7 +560,7 @@ var $AnimateProvider = ['$provide', /** @this */ function($provide) {
* - **removeClass** - `{string}` - space-separated CSS classes to remove from element
* - **to** - `{Object}` - CSS properties & values at end of animation. Must have matching `from`
*
* @return {Promise} the animation callback promise
* @return {Runner} the animation runner
*/
enter: function(element, parent, after, options) {
parent = parent && jqLite(parent);
Expand Down Expand Up @@ -528,7 +592,7 @@ var $AnimateProvider = ['$provide', /** @this */ function($provide) {
* - **removeClass** - `{string}` - space-separated CSS classes to remove from element
* - **to** - `{Object}` - CSS properties & values at end of animation. Must have matching `from`
*
* @return {Promise} the animation callback promise
* @return {Runner} the animation runner
*/
move: function(element, parent, after, options) {
parent = parent && jqLite(parent);
Expand All @@ -555,7 +619,7 @@ var $AnimateProvider = ['$provide', /** @this */ function($provide) {
* - **removeClass** - `{string}` - space-separated CSS classes to remove from element
* - **to** - `{Object}` - CSS properties & values at end of animation. Must have matching `from`
*
* @return {Promise} the animation callback promise
* @return {Runner} the animation runner
*/
leave: function(element, options) {
return $$animateQueue.push(element, 'leave', prepareAnimateOptions(options), function() {
Expand Down Expand Up @@ -585,7 +649,7 @@ var $AnimateProvider = ['$provide', /** @this */ function($provide) {
* - **removeClass** - `{string}` - space-separated CSS classes to remove from element
* - **to** - `{Object}` - CSS properties & values at end of animation. Must have matching `from`
*
* @return {Promise} the animation callback promise
* @return {Runner} animationRunner the animation runner
*/
addClass: function(element, className, options) {
options = prepareAnimateOptions(options);
Expand Down Expand Up @@ -615,7 +679,7 @@ var $AnimateProvider = ['$provide', /** @this */ function($provide) {
* - **removeClass** - `{string}` - space-separated CSS classes to remove from element
* - **to** - `{Object}` - CSS properties & values at end of animation. Must have matching `from`
*
* @return {Promise} the animation callback promise
* @return {Runner} the animation runner
*/
removeClass: function(element, className, options) {
options = prepareAnimateOptions(options);
Expand Down Expand Up @@ -646,7 +710,7 @@ var $AnimateProvider = ['$provide', /** @this */ function($provide) {
* - **removeClass** - `{string}` - space-separated CSS classes to remove from element
* - **to** - `{Object}` - CSS properties & values at end of animation. Must have matching `from`
*
* @return {Promise} the animation callback promise
* @return {Runner} the animation runner
*/
setClass: function(element, add, remove, options) {
options = prepareAnimateOptions(options);
Expand Down Expand Up @@ -693,7 +757,7 @@ var $AnimateProvider = ['$provide', /** @this */ function($provide) {
* - **removeClass** - `{string}` - space-separated CSS classes to remove from element
* - **to** - `{Object}` - CSS properties & values at end of animation. Must have matching `from`
*
* @return {Promise} the animation callback promise
* @return {Runner} the animation runner
*/
animate: function(element, from, to, className, options) {
options = prepareAnimateOptions(options);
Expand Down
190 changes: 190 additions & 0 deletions test/ngAnimate/animateSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -790,6 +790,7 @@ describe('animations', function() {
expect(element).toHaveClass('red');
}));


it('removeClass() should issue a removeClass animation with the correct DOM operation', inject(function($animate, $rootScope) {
parent.append(element);
element.addClass('blue');
Expand Down Expand Up @@ -934,6 +935,195 @@ describe('animations', function() {
}));
});


describe('$animate.cancel()', function() {

it('should cancel enter()', inject(function($animate, $rootScope) {
expect(parent.children().length).toBe(0);

options.foo = 'bar';
var spy = jasmine.createSpy('cancelCatch');

var runner = $animate.enter(element, parent, null, options);

runner.catch(spy);

expect(parent.children().length).toBe(1);

$rootScope.$digest();

expect(capturedAnimation[0]).toBe(element);
expect(capturedAnimation[1]).toBe('enter');
expect(capturedAnimation[2].foo).toEqual(options.foo);

$animate.cancel(runner);
// Since enter() immediately adds the element, we can only check if the
// element is still at the position
expect(parent.children().length).toBe(1);

$rootScope.$digest();

// Catch handler is called after digest
expect(spy).toHaveBeenCalled();
}));


it('should cancel move()', inject(function($animate, $rootScope) {
parent.append(element);

expect(parent.children().length).toBe(1);
expect(parent2.children().length).toBe(0);

options.foo = 'bar';
var spy = jasmine.createSpy('cancelCatch');

var runner = $animate.move(element, parent2, null, options);
runner.catch(spy);

expect(parent.children().length).toBe(0);
expect(parent2.children().length).toBe(1);

$rootScope.$digest();

expect(capturedAnimation[0]).toBe(element);
expect(capturedAnimation[1]).toBe('move');
expect(capturedAnimation[2].foo).toEqual(options.foo);

$animate.cancel(runner);
// Since moves() immediately moves the element, we can only check if the
// element is still at the correct position
expect(parent.children().length).toBe(0);
expect(parent2.children().length).toBe(1);

$rootScope.$digest();

// Catch handler is called after digest
expect(spy).toHaveBeenCalled();
}));


it('cancel leave()', inject(function($animate, $rootScope) {
parent.append(element);
options.foo = 'bar';
var spy = jasmine.createSpy('cancelCatch');

var runner = $animate.leave(element, options);

runner.catch(spy);
$rootScope.$digest();

expect(capturedAnimation[0]).toBe(element);
expect(capturedAnimation[1]).toBe('leave');
expect(capturedAnimation[2].foo).toEqual(options.foo);

expect(element.parent().length).toBe(1);

$animate.cancel(runner);
// Animation concludes immediately
expect(element.parent().length).toBe(0);
expect(spy).not.toHaveBeenCalled();

$rootScope.$digest();
// Catch handler is called after digest
expect(spy).toHaveBeenCalled();
}));

it('should cancel addClass()', inject(function($animate, $rootScope) {
parent.append(element);
options.foo = 'bar';
var runner = $animate.addClass(element, 'red', options);
var spy = jasmine.createSpy('cancelCatch');

runner.catch(spy);
$rootScope.$digest();

expect(capturedAnimation[0]).toBe(element);
expect(capturedAnimation[1]).toBe('addClass');
expect(capturedAnimation[2].foo).toEqual(options.foo);

$animate.cancel(runner);
expect(element).toHaveClass('red');
expect(spy).not.toHaveBeenCalled();

$rootScope.$digest();
expect(spy).toHaveBeenCalled();
}));


it('should cancel setClass()', inject(function($animate, $rootScope) {
parent.append(element);
element.addClass('red');
options.foo = 'bar';

var runner = $animate.setClass(element, 'blue', 'red', options);
var spy = jasmine.createSpy('cancelCatch');

runner.catch(spy);
$rootScope.$digest();

expect(capturedAnimation[0]).toBe(element);
expect(capturedAnimation[1]).toBe('setClass');
expect(capturedAnimation[2].foo).toEqual(options.foo);

$animate.cancel(runner);
expect(element).toHaveClass('blue');
expect(element).not.toHaveClass('red');
expect(spy).not.toHaveBeenCalled();

$rootScope.$digest();
expect(spy).toHaveBeenCalled();
}));


it('should cancel removeClass()', inject(function($animate, $rootScope) {
parent.append(element);
element.addClass('red blue');

options.foo = 'bar';
var runner = $animate.removeClass(element, 'red', options);
var spy = jasmine.createSpy('cancelCatch');

runner.catch(spy);
$rootScope.$digest();

expect(capturedAnimation[0]).toBe(element);
expect(capturedAnimation[1]).toBe('removeClass');
expect(capturedAnimation[2].foo).toEqual(options.foo);

$animate.cancel(runner);
expect(element).not.toHaveClass('red');
expect(element).toHaveClass('blue');

$rootScope.$digest();
expect(spy).toHaveBeenCalled();
}));


it('should cancel animate()',
inject(function($animate, $rootScope) {

parent.append(element);

var fromStyle = { color: 'blue' };
var options = { addClass: 'red' };

var runner = $animate.animate(element, fromStyle, null, null, options);
var spy = jasmine.createSpy('cancelCatch');

runner.catch(spy);
$rootScope.$digest();

expect(capturedAnimation).toBeTruthy();

$animate.cancel(runner);
expect(element).toHaveClass('red');

$rootScope.$digest();
expect(spy).toHaveBeenCalled();
}));
});


describe('parent animations', function() {
they('should not cancel a pre-digest parent class-based animation if a child $prop animation is set to run',
['structural', 'class-based'], function(animationType) {
Expand Down

0 comments on commit 16b82c6

Please sign in to comment.