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.
This commit is contained in:
+75
-11
@@ -464,13 +464,77 @@ var $AnimateProvider = ['$provide', /** @this */ function($provide) {
|
||||
* @ngdoc method
|
||||
* @name $animate#cancel
|
||||
* @kind function
|
||||
* @description Cancels the provided animation.
|
||||
* @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 {Promise} animationPromise The animation promise that is returned when an animation is started.
|
||||
* @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();
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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() {
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
|
||||
@@ -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');
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user