diff --git a/src/ng/animate.js b/src/ng/animate.js
index f404eccc5461..584358366dc8 100644
--- a/src/ng/animate.js
+++ b/src/ng/animate.js
@@ -81,10 +81,20 @@ var $AnimateProvider = ['$provide', function($provide) {
return this.$$classNameFilter;
};
- this.$get = ['$timeout', '$$asyncCallback', function($timeout, $$asyncCallback) {
-
- function async(fn) {
- fn && $$asyncCallback(fn);
+ this.$get = ['$$q', '$$asyncCallback', function($$q, $$asyncCallback) {
+
+ var currentDefer;
+ function asyncPromise() {
+ // only serve one instance of a promise in order to save CPU cycles
+ if (!currentDefer) {
+ currentDefer = $$q.defer();
+ currentDefer.promise.cancel = noop; //ngAnimate.$animate provides this
+ $$asyncCallback(function() {
+ currentDefer.resolve();
+ currentDefer = null;
+ });
+ }
+ return currentDefer.promise;
}
/**
@@ -112,22 +122,19 @@ var $AnimateProvider = ['$provide', function($provide) {
* @name $animate#enter
* @kind function
* @description Inserts the element into the DOM either after the `after` element or
- * as the first child within the `parent` element. Once complete, the done() callback
- * will be fired (if provided).
+ * as the first child within the `parent` element. When the function is called a promise
+ * is returned that will be resolved at a later time.
* @param {DOMElement} element the element which will be inserted into the DOM
* @param {DOMElement} parent the parent element which will append the element as
* a child (if the after element is not present)
* @param {DOMElement} after the sibling element which will append the element
* after itself
- * @param {Function=} done callback function that will be called after the element has been
- * inserted into the DOM
+ * @return {Promise} the animation callback promise
*/
- enter : function(element, parent, after, done) {
- after
- ? after.after(element)
- : parent.prepend(element);
- async(done);
- return noop;
+ enter : function(element, parent, after) {
+ after ? after.after(element)
+ : parent.prepend(element);
+ return asyncPromise();
},
/**
@@ -135,16 +142,14 @@ var $AnimateProvider = ['$provide', function($provide) {
* @ngdoc method
* @name $animate#leave
* @kind function
- * @description Removes the element from the DOM. Once complete, the done() callback will be
- * fired (if provided).
+ * @description Removes the element from the DOM. When the function is called a promise
+ * is returned that will be resolved at a later time.
* @param {DOMElement} element the element which will be removed from the DOM
- * @param {Function=} done callback function that will be called after the element has been
- * removed from the DOM
+ * @return {Promise} the animation callback promise
*/
- leave : function(element, done) {
+ leave : function(element) {
element.remove();
- async(done);
- return noop;
+ return asyncPromise();
},
/**
@@ -153,8 +158,8 @@ var $AnimateProvider = ['$provide', function($provide) {
* @name $animate#move
* @kind function
* @description Moves the position of the provided element within the DOM to be placed
- * either after the `after` element or inside of the `parent` element. Once complete, the
- * done() callback will be fired (if provided).
+ * either after the `after` element or inside of the `parent` element. When the function
+ * is called a promise is returned that will be resolved at a later time.
*
* @param {DOMElement} element the element which will be moved around within the
* DOM
@@ -162,13 +167,12 @@ var $AnimateProvider = ['$provide', function($provide) {
* inserted into (if the after element is not present)
* @param {DOMElement} after the sibling element where the element will be
* positioned next to
- * @param {Function=} done the callback function (if provided) that will be fired after the
- * element has been moved to its new position
+ * @return {Promise} the animation callback promise
*/
- move : function(element, parent, after, done) {
+ move : function(element, parent, after) {
// Do not remove element before insert. Removing will cause data associated with the
// element to be dropped. Insert will implicitly do the remove.
- return this.enter(element, parent, after, done);
+ return this.enter(element, parent, after);
},
/**
@@ -176,23 +180,21 @@ var $AnimateProvider = ['$provide', function($provide) {
* @ngdoc method
* @name $animate#addClass
* @kind function
- * @description Adds the provided className CSS class value to the provided element. Once
- * complete, the done() callback will be fired (if provided).
+ * @description Adds the provided className CSS class value to the provided element.
+ * When the function is called a promise is returned that will be resolved at a later time.
* @param {DOMElement} element the element which will have the className value
* added to it
* @param {string} className the CSS class which will be added to the element
- * @param {Function=} done the callback function (if provided) that will be fired after the
- * className value has been added to the element
+ * @return {Promise} the animation callback promise
*/
- addClass : function(element, className, done) {
+ addClass : function(element, className) {
className = !isString(className)
? (isArray(className) ? className.join(' ') : '')
: className;
forEach(element, function (element) {
jqLiteAddClass(element, className);
});
- async(done);
- return noop;
+ return asyncPromise();
},
/**
@@ -201,22 +203,20 @@ var $AnimateProvider = ['$provide', function($provide) {
* @name $animate#removeClass
* @kind function
* @description Removes the provided className CSS class value from the provided element.
- * Once complete, the done() callback will be fired (if provided).
+ * When the function is called a promise is returned that will be resolved at a later time.
* @param {DOMElement} element the element which will have the className value
* removed from it
* @param {string} className the CSS class which will be removed from the element
- * @param {Function=} done the callback function (if provided) that will be fired after the
- * className value has been removed from the element
+ * @return {Promise} the animation callback promise
*/
- removeClass : function(element, className, done) {
- className = isString(className) ?
- className :
- isArray(className) ? className.join(' ') : '';
+ removeClass : function(element, className) {
+ className = !isString(className)
+ ? (isArray(className) ? className.join(' ') : '')
+ : className;
forEach(element, function (element) {
jqLiteRemoveClass(element, className);
});
- async(done);
- return noop;
+ return asyncPromise();
},
/**
@@ -225,21 +225,17 @@ var $AnimateProvider = ['$provide', function($provide) {
* @name $animate#setClass
* @kind function
* @description Adds and/or removes the given CSS classes to and from the element.
- * Once complete, the done() callback will be fired (if provided).
+ * When the function is called a promise is returned that will be resolved at a later time.
* @param {DOMElement} element the element which will have its CSS classes changed
* removed from it
* @param {string} add the CSS classes which will be added to the element
* @param {string} remove the CSS class which will be removed from the element
- * @param {Function=} done the callback function (if provided) that will be fired after the
- * CSS classes have been set on the element
+ * @return {Promise} the animation callback promise
*/
- setClass : function(element, add, remove, done) {
- forEach(element, function (element) {
- jqLiteAddClass(element, add);
- jqLiteRemoveClass(element, remove);
- });
- async(done);
- return noop;
+ setClass : function(element, add, remove) {
+ this.addClass(element, add);
+ this.removeClass(element, remove);
+ return asyncPromise();
},
enabled : noop
diff --git a/src/ng/compile.js b/src/ng/compile.js
index 175efc13211e..6b3e075b4a43 100644
--- a/src/ng/compile.js
+++ b/src/ng/compile.js
@@ -720,14 +720,13 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
*/
$updateClass : function(newClasses, oldClasses) {
var toAdd = tokenDifference(newClasses, oldClasses);
- var toRemove = tokenDifference(oldClasses, newClasses);
+ if (toAdd && toAdd.length) {
+ $animate.addClass(this.$$element, toAdd);
+ }
- if(toAdd.length === 0) {
+ var toRemove = tokenDifference(oldClasses, newClasses);
+ if (toRemove && toRemove.length) {
$animate.removeClass(this.$$element, toRemove);
- } else if(toRemove.length === 0) {
- $animate.addClass(this.$$element, toAdd);
- } else {
- $animate.setClass(this.$$element, toAdd, toRemove);
}
},
diff --git a/src/ng/directive/ngClass.js b/src/ng/directive/ngClass.js
index c9550187576f..71dfb81f9c28 100644
--- a/src/ng/directive/ngClass.js
+++ b/src/ng/directive/ngClass.js
@@ -56,15 +56,13 @@ function classDirective(name, selector) {
function updateClasses (oldClasses, newClasses) {
var toAdd = arrayDifference(newClasses, oldClasses);
var toRemove = arrayDifference(oldClasses, newClasses);
- toRemove = digestClassCounts(toRemove, -1);
toAdd = digestClassCounts(toAdd, 1);
-
- if (toAdd.length === 0) {
- $animate.removeClass(element, toRemove);
- } else if (toRemove.length === 0) {
+ toRemove = digestClassCounts(toRemove, -1);
+ if(toAdd && toAdd.length) {
$animate.addClass(element, toAdd);
- } else {
- $animate.setClass(element, toAdd, toRemove);
+ }
+ if(toRemove && toRemove.length) {
+ $animate.removeClass(element, toRemove);
}
}
diff --git a/src/ng/directive/ngIf.js b/src/ng/directive/ngIf.js
index b4c569fecbbe..34cec3696fa4 100644
--- a/src/ng/directive/ngIf.js
+++ b/src/ng/directive/ngIf.js
@@ -113,7 +113,7 @@ var ngIfDirective = ['$animate', function($animate) {
}
if(block) {
previousElements = getBlockElements(block.clone);
- $animate.leave(previousElements, function() {
+ $animate.leave(previousElements).then(function() {
previousElements = null;
});
block = null;
diff --git a/src/ng/directive/ngInclude.js b/src/ng/directive/ngInclude.js
index 7b0d020bf5f2..8aea896b2dd8 100644
--- a/src/ng/directive/ngInclude.js
+++ b/src/ng/directive/ngInclude.js
@@ -198,7 +198,7 @@ var ngIncludeDirective = ['$http', '$templateCache', '$anchorScroll', '$animate'
currentScope = null;
}
if(currentElement) {
- $animate.leave(currentElement, function() {
+ $animate.leave(currentElement).then(function() {
previousElement = null;
});
previousElement = currentElement;
@@ -228,7 +228,7 @@ var ngIncludeDirective = ['$http', '$templateCache', '$anchorScroll', '$animate'
// directives to non existing elements.
var clone = $transclude(newScope, function(clone) {
cleanupLastIncludeContent();
- $animate.enter(clone, null, $element, afterAnimation);
+ $animate.enter(clone, null, $element).then(afterAnimation);
});
currentScope = newScope;
diff --git a/src/ng/directive/ngSwitch.js b/src/ng/directive/ngSwitch.js
index 1eb1d32a4e21..4644e3194712 100644
--- a/src/ng/directive/ngSwitch.js
+++ b/src/ng/directive/ngSwitch.js
@@ -155,7 +155,7 @@ var ngSwitchDirective = ['$animate', function($animate) {
var selected = getBlockElements(selectedElements[i].clone);
selectedScopes[i].$destroy();
previousElements[i] = selected;
- $animate.leave(selected, function() {
+ $animate.leave(selected).then(function() {
previousElements.splice(i, 1);
});
}
diff --git a/src/ngAnimate/animate.js b/src/ngAnimate/animate.js
index eeffc16f4e1d..de119f62bdea 100644
--- a/src/ngAnimate/animate.js
+++ b/src/ngAnimate/animate.js
@@ -264,7 +264,7 @@
*
* Stagger animations are currently only supported within CSS-defined animations.
*
- *
JavaScript-defined Animations
+ * ## JavaScript-defined Animations
* In the event that you do not want to use CSS3 transitions or CSS3 animations or if you wish to offer animations on browsers that do not
* yet support CSS transitions/animations, then you can make use of JavaScript animations defined inside of your AngularJS module.
*
@@ -366,6 +366,7 @@ angular.module('ngAnimate', ['ng'])
var noop = angular.noop;
var forEach = angular.forEach;
var selectors = $animateProvider.$$selectors;
+ var isArray = angular.isArray;
var ELEMENT_NODE = 1;
var NG_ANIMATE_STATE = '$$ngAnimateState';
@@ -394,8 +395,13 @@ angular.module('ngAnimate', ['ng'])
return extractElementNode(elm1) == extractElementNode(elm2);
}
- $provide.decorator('$animate', ['$delegate', '$injector', '$sniffer', '$rootElement', '$$asyncCallback', '$rootScope', '$document',
- function($delegate, $injector, $sniffer, $rootElement, $$asyncCallback, $rootScope, $document) {
+ function isEmpty(val) {
+ return !val && val.length === 0;
+ }
+
+ $provide.decorator('$animate',
+ ['$delegate', '$$q', '$injector', '$sniffer', '$rootElement', '$$asyncCallback', '$rootScope', '$document',
+ function($delegate, $$q, $injector, $sniffer, $rootElement, $$asyncCallback, $rootScope, $document) {
var globalAnimationCounter = 0;
$rootElement.data(NG_ANIMATE_STATE, rootAnimateState);
@@ -419,20 +425,72 @@ angular.module('ngAnimate', ['ng'])
return classNameFilter.test(className);
};
- function blockElementAnimations(element) {
+ function classBasedAnimationsBlocked(element, setter) {
var data = element.data(NG_ANIMATE_STATE) || {};
- data.running = true;
- element.data(NG_ANIMATE_STATE, data);
+ if (setter) {
+ data.running = true;
+ data.structural = true;
+ element.data(NG_ANIMATE_STATE, data);
+ }
+ return data.disabled || (data.running && data.structural);
}
function runAnimationPostDigest(fn) {
- var cancelFn;
- $rootScope.$$postDigest(function() {
- cancelFn = fn();
- });
- return function() {
+ var cancelFn, defer = $$q.defer();
+ defer.promise.cancel = function() {
cancelFn && cancelFn();
};
+ $rootScope.$$postDigest(function() {
+ cancelFn = fn(function() {
+ defer.resolve();
+ });
+ });
+ return defer.promise;
+ }
+
+ function resolveElementClasses(element, cache, runningAnimations) {
+ runningAnimations = runningAnimations || {};
+ var map = {};
+
+ forEach(cache.add, function(className) {
+ if(className && className.length) {
+ map[className] = map[className] || 0;
+ map[className]++;
+ }
+ });
+
+ forEach(cache.remove, function(className) {
+ if(className && className.length) {
+ map[className] = map[className] || 0;
+ map[className]--;
+ }
+ });
+
+ var lookup = [];
+ forEach(runningAnimations, function(data, selector) {
+ forEach(selector.split(' '), function(s) {
+ lookup[s]=data;
+ });
+ });
+
+ var toAdd = [], toRemove = [];
+ forEach(map, function(status, className) {
+ var hasClass = element.hasClass(className);
+ var matchingAnimation = lookup[className] || {};
+ if (status < 0) {
+ //does it have the class or will it have the class
+ if(hasClass || matchingAnimation.event == 'addClass') {
+ toRemove.push(className);
+ }
+ } else if (status > 0) {
+ //is the class missing or will it be removed?
+ if(!hasClass || matchingAnimation.event == 'removeClass') {
+ toAdd.push(className);
+ }
+ }
+ });
+
+ return (toAdd.length + toRemove.length) > 0 && [toAdd.join(' '), toRemove.join(' ')];
}
function lookup(name) {
@@ -473,17 +531,26 @@ angular.module('ngAnimate', ['ng'])
return;
}
+ var classNameAdd, classNameRemove;
+ if(isArray(className)) {
+ classNameAdd = className[0];
+ classNameRemove = className[1];
+ if (isEmpty(classNameAdd)) {
+ className = classNameRemove;
+ animationEvent = 'removeClass';
+ } else if(isEmpty(classNameRemove)) {
+ className = classNameAdd;
+ animationEvent = 'addClass';
+ } else {
+ className = classNameAdd + ' ' + classNameRemove;
+ }
+ }
+
var isSetClassOperation = animationEvent == 'setClass';
var isClassBased = isSetClassOperation ||
animationEvent == 'addClass' ||
animationEvent == 'removeClass';
- var classNameAdd, classNameRemove;
- if(angular.isArray(className)) {
- classNameAdd = className[0];
- classNameRemove = className[1];
- className = classNameAdd + ' ' + classNameRemove;
- }
var currentClassName = element.attr('class');
var classes = currentClassName + ' ' + className;
@@ -610,7 +677,7 @@ angular.module('ngAnimate', ['ng'])
/**
* @ngdoc service
* @name $animate
- * @kind function
+ * @kind object
*
* @description
* The `$animate` service provides animation detection support while performing DOM operations (enter, leave and move) as well as during addClass and removeClass operations.
@@ -624,6 +691,45 @@ angular.module('ngAnimate', ['ng'])
* Requires the {@link ngAnimate `ngAnimate`} module to be installed.
*
* Please visit the {@link ngAnimate `ngAnimate`} module overview page learn more about how to use animations in your application.
+ * ## Callback Promises
+ * With AngularJS 1.3, each of the animation methods, on the `$animate` service, return a promise when called. The
+ * promise itself is then resolved once the animation has completed itself, has been cancelled or has been
+ * skipped due to animations being disabled. (Note that even if the animation is cancelled it will still
+ * call the resolve function of the animation.)
+ *
+ * ```js
+ * $animate.enter(element, container).then(function() {
+ * //...this is called once the animation is complete...
+ * });
+ * ```
+ *
+ * Also note that, due to the nature of the callback promise, if any Angular-specific code (like changing the scope,
+ * location of the page, etc...) is executed within the callback promise then be sure to wrap the code using
+ * `$scope.$apply(...)`;
+ *
+ * ```js
+ * $animate.leave(element).then(function() {
+ * $scope.$apply(function() {
+ * $location.path('/new-page');
+ * });
+ * });
+ * ```
+ *
+ * An animation can also be cancelled by calling the `cancel()` method on the returned promise.
+ *
+ * ```js
+ * var promise = $animate.addClass(element, 'super-long-animation').then(function() {
+ * //this will still be called even if cancelled
+ * });
+ *
+ * element.on('click', function() {
+ * //tooo lazy to wait for the animation to end
+ * promise.cancel();
+ * });
+ * ```
+ *
+ * (Keep in mind that the cancel function is unique to `$animate` and promises in general due not provide support
+ * for calling cancel.)
*
*/
return {
@@ -652,23 +758,22 @@ angular.module('ngAnimate', ['ng'])
* | 10. the .ng-enter-active class is added (this triggers the CSS transition/animation) | class="my-animation ng-animate ng-enter ng-enter-active" |
* | 11. $animate waits for the animation to complete (via events and timeout) | class="my-animation ng-animate ng-enter ng-enter-active" |
* | 12. The animation ends and all generated CSS classes are removed from the element | class="my-animation" |
- * | 13. The doneCallback() callback is fired (if provided) | class="my-animation" |
+ * | 13. The returned promise is resolved. | class="my-animation" |
*
* @param {DOMElement} element the element that will be the focus of the enter animation
* @param {DOMElement} parentElement the parent element of the element that will be the focus of the enter animation
* @param {DOMElement} afterElement the sibling element (which is the previous element) of the element that will be the focus of the enter animation
- * @param {function()=} doneCallback the callback function that will be called once the animation is complete
- * @return {function} the animation cancellation function
+ * @return {Promise} the animation callback promise
*/
- enter : function(element, parentElement, afterElement, doneCallback) {
+ enter : function(element, parentElement, afterElement) {
element = angular.element(element);
parentElement = prepareElement(parentElement);
afterElement = prepareElement(afterElement);
- blockElementAnimations(element);
+ classBasedAnimationsBlocked(element, true);
$delegate.enter(element, parentElement, afterElement);
- return runAnimationPostDigest(function() {
- return performAnimation('enter', 'ng-enter', stripCommentsFromElement(element), parentElement, afterElement, noop, doneCallback);
+ return runAnimationPostDigest(function(done) {
+ return performAnimation('enter', 'ng-enter', stripCommentsFromElement(element), parentElement, afterElement, noop, done);
});
},
@@ -697,22 +802,21 @@ angular.module('ngAnimate', ['ng'])
* | 10. $animate waits for the animation to complete (via events and timeout) | class="my-animation ng-animate ng-leave ng-leave-active" |
* | 11. The animation ends and all generated CSS classes are removed from the element | class="my-animation" |
* | 12. The element is removed from the DOM | ... |
- * | 13. The doneCallback() callback is fired (if provided) | ... |
+ * | 13. The returned promise is resolved. | ... |
*
* @param {DOMElement} element the element that will be the focus of the leave animation
- * @param {function()=} doneCallback the callback function that will be called once the animation is complete
- * @return {function} the animation cancellation function
+ * @return {Promise} the animation callback promise
*/
- leave : function(element, doneCallback) {
+ leave : function(element) {
element = angular.element(element);
cancelChildAnimations(element);
- blockElementAnimations(element);
+ classBasedAnimationsBlocked(element, true);
this.enabled(false, element);
- return runAnimationPostDigest(function() {
+ return runAnimationPostDigest(function(done) {
return performAnimation('leave', 'ng-leave', stripCommentsFromElement(element), null, null, function() {
$delegate.leave(element);
- }, doneCallback);
+ }, done);
});
},
@@ -742,24 +846,23 @@ angular.module('ngAnimate', ['ng'])
* | 10. the .ng-move-active class is added (this triggers the CSS transition/animation) | class="my-animation ng-animate ng-move ng-move-active" |
* | 11. $animate waits for the animation to complete (via events and timeout) | class="my-animation ng-animate ng-move ng-move-active" |
* | 12. The animation ends and all generated CSS classes are removed from the element | class="my-animation" |
- * | 13. The doneCallback() callback is fired (if provided) | class="my-animation" |
+ * | 13. The returned promise is resolved. | class="my-animation" |
*
* @param {DOMElement} element the element that will be the focus of the move animation
* @param {DOMElement} parentElement the parentElement element of the element that will be the focus of the move animation
* @param {DOMElement} afterElement the sibling element (which is the previous element) of the element that will be the focus of the move animation
- * @param {function()=} doneCallback the callback function that will be called once the animation is complete
- * @return {function} the animation cancellation function
+ * @return {Promise} the animation callback promise
*/
- move : function(element, parentElement, afterElement, doneCallback) {
+ move : function(element, parentElement, afterElement) {
element = angular.element(element);
parentElement = prepareElement(parentElement);
afterElement = prepareElement(afterElement);
cancelChildAnimations(element);
- blockElementAnimations(element);
+ classBasedAnimationsBlocked(element, true);
$delegate.move(element, parentElement, afterElement);
- return runAnimationPostDigest(function() {
- return performAnimation('move', 'ng-move', stripCommentsFromElement(element), parentElement, afterElement, noop, doneCallback);
+ return runAnimationPostDigest(function(done) {
+ return performAnimation('move', 'ng-move', stripCommentsFromElement(element), parentElement, afterElement, noop, done);
});
},
@@ -786,19 +889,14 @@ angular.module('ngAnimate', ['ng'])
* | 7. $animate waits for the animation to complete (via events and timeout) | class="my-animation super super-add super-add-active" |
* | 8. The animation ends and all generated CSS classes are removed from the element | class="my-animation super" |
* | 9. The super class is kept on the element | class="my-animation super" |
- * | 10. The doneCallback() callback is fired (if provided) | class="my-animation super" |
+ * | 10. The returned promise is resolved. | class="my-animation super" |
*
* @param {DOMElement} element the element that will be animated
* @param {string} className the CSS class that will be added to the element and then animated
- * @param {function()=} doneCallback the callback function that will be called once the animation is complete
- * @return {function} the animation cancellation function
+ * @return {Promise} the animation callback promise
*/
- addClass : function(element, className, doneCallback) {
- element = angular.element(element);
- element = stripCommentsFromElement(element);
- return performAnimation('addClass', className, element, null, null, function() {
- $delegate.addClass(element, className);
- }, doneCallback);
+ addClass : function(element, className) {
+ return this.setClass(element, className, []);
},
/**
@@ -823,20 +921,15 @@ angular.module('ngAnimate', ['ng'])
* | 6. $animate scans the element styles to get the CSS transition/animation duration and delay | class="my-animation super ng-animate super-remove" |
* | 7. $animate waits for the animation to complete (via events and timeout) | class="my-animation ng-animate super-remove super-remove-active" |
* | 8. The animation ends and all generated CSS classes are removed from the element | class="my-animation" |
- * | 9. The doneCallback() callback is fired (if provided) | class="my-animation" |
+ * | 9. The returned promise is resolved. | class="my-animation" |
*
*
* @param {DOMElement} element the element that will be animated
* @param {string} className the CSS class that will be animated and then removed from the element
- * @param {function()=} doneCallback the callback function that will be called once the animation is complete
- * @return {function} the animation cancellation function
+ * @return {Promise} the animation callback promise
*/
- removeClass : function(element, className, doneCallback) {
- element = angular.element(element);
- element = stripCommentsFromElement(element);
- return performAnimation('removeClass', className, element, null, null, function() {
- $delegate.removeClass(element, className);
- }, doneCallback);
+ removeClass : function(element, className) {
+ return this.setClass(element, [], className);
},
/**
@@ -856,23 +949,54 @@ angular.module('ngAnimate', ['ng'])
* | 5. the .on, .on-add-active and .off-remove-active classes are added and .off is removed (this triggers the CSS transition/animation) | class="my-animation ng-animate on on-add on-add-active off-remove off-remove-active” |
* | 6. $animate scans the element styles to get the CSS transition/animation duration and delay | class="my-animation ng-animate on on-add on-add-active off-remove off-remove-active" |
* | 7. $animate waits for the animation to complete (via events and timeout) | class="my-animation ng-animate on on-add on-add-active off-remove off-remove-active" |
- * | 8. The animation ends and all generated CSS classes are removed from the element | class="my-animation" |
- * | 9. The doneCallback() callback is fired (if provided) | class="my-animation" |
+ * | 8. The animation ends and all generated CSS classes are removed from the element | class="my-animation on" |
+ * | 9. The returned promise is resolved. | class="my-animation on" |
*
* @param {DOMElement} element the element which will have its CSS classes changed
* removed from it
* @param {string} add the CSS classes which will be added to the element
* @param {string} remove the CSS class which will be removed from the element
- * @param {function=} done the callback function (if provided) that will be fired after the
* CSS classes have been set on the element
- * @return {function} the animation cancellation function
+ * @return {Promise} the animation callback promise
*/
- setClass : function(element, add, remove, doneCallback) {
+ setClass : function(element, add, remove) {
+ var STORAGE_KEY = '$$animateClasses';
element = angular.element(element);
element = stripCommentsFromElement(element);
- return performAnimation('setClass', [add, remove], element, null, null, function() {
- $delegate.setClass(element, add, remove);
- }, doneCallback);
+
+ if(classBasedAnimationsBlocked(element)) {
+ return $delegate.setClass(element, add, remove);
+ }
+
+ add = isArray(add) ? add : add.split(' ');
+ remove = isArray(remove) ? remove : remove.split(' ');
+
+ var cache = element.data(STORAGE_KEY);
+ if (cache) {
+ cache.add = cache.add.concat(add);
+ cache.remove = cache.remove.concat(remove);
+
+ //the digest cycle will combine all the animations into one function
+ return cache.promise;
+ } else {
+ element.data(STORAGE_KEY, cache = {
+ add : add,
+ remove : remove
+ });
+ }
+
+ return cache.promise = runAnimationPostDigest(function(done) {
+ var cache = element.data(STORAGE_KEY);
+ element.removeData(STORAGE_KEY);
+
+ var state = element.data(NG_ANIMATE_STATE) || {};
+ var classes = resolveElementClasses(element, cache, state.active);
+ return !classes
+ ? done()
+ : performAnimation('setClass', classes, element, null, null, function() {
+ $delegate.setClass(element, classes[0], classes[1]);
+ }, done);
+ });
},
/**
@@ -931,6 +1055,7 @@ angular.module('ngAnimate', ['ng'])
return noopCancel;
}
+ animationEvent = runner.event;
className = runner.className;
var elementEvents = angular.element._data(runner.node);
elementEvents = elementEvents && elementEvents.events;
@@ -939,25 +1064,11 @@ angular.module('ngAnimate', ['ng'])
parentElement = afterElement ? afterElement.parent() : element.parent();
}
- var ngAnimateState = element.data(NG_ANIMATE_STATE) || {};
- var runningAnimations = ngAnimateState.active || {};
- var totalActiveAnimations = ngAnimateState.totalActive || 0;
- var lastAnimation = ngAnimateState.last;
-
- //only allow animations if the currently running animation is not structural
- //or if there is no animation running at all
- var skipAnimations;
- if (runner.isClassBased) {
- skipAnimations = ngAnimateState.running ||
- ngAnimateState.disabled ||
- (lastAnimation && !lastAnimation.isClassBased);
- }
-
//skip the animation if animations are disabled, a parent is already being animated,
//the element is not currently attached to the document body or then completely close
//the animation if any matching animations are not found at all.
//NOTE: IE8 + IE9 should close properly (run closeAnimation()) in case an animation was found.
- if (skipAnimations || animationsDisabled(element, parentElement)) {
+ if (animationsDisabled(element, parentElement)) {
fireDOMOperation();
fireBeforeCallbackAsync();
fireAfterCallbackAsync();
@@ -965,7 +1076,12 @@ angular.module('ngAnimate', ['ng'])
return noopCancel;
}
+ var ngAnimateState = element.data(NG_ANIMATE_STATE) || {};
+ var runningAnimations = ngAnimateState.active || {};
+ var totalActiveAnimations = ngAnimateState.totalActive || 0;
+ var lastAnimation = ngAnimateState.last;
var skipAnimation = false;
+
if(totalActiveAnimations > 0) {
var animationsToCancel = [];
if(!runner.isClassBased) {
@@ -1000,9 +1116,6 @@ angular.module('ngAnimate', ['ng'])
}
}
- runningAnimations = ngAnimateState.active || {};
- totalActiveAnimations = ngAnimateState.totalActive || 0;
-
if(runner.isClassBased && !runner.isSetClassOperation && !skipAnimation) {
skipAnimation = (animationEvent == 'addClass') == element.hasClass(className); //opposite of XOR
}
@@ -1015,6 +1128,9 @@ angular.module('ngAnimate', ['ng'])
return noopCancel;
}
+ runningAnimations = ngAnimateState.active || {};
+ totalActiveAnimations = ngAnimateState.totalActive || 0;
+
if(animationEvent == 'leave') {
//there's no need to ever remove the listener since the element
//will be removed (destroyed) after the leave animation ends or
@@ -1089,11 +1205,7 @@ angular.module('ngAnimate', ['ng'])
function fireDoneCallbackAsync() {
fireDOMCallback('close');
- if(doneCallback) {
- $$asyncCallback(function() {
- doneCallback();
- });
- }
+ doneCallback();
}
//it is less complicated to use a flag than managing and canceling
@@ -1474,14 +1586,15 @@ angular.module('ngAnimate', ['ng'])
}
var activeClassName = '';
+ var pendingClassName = '';
forEach(className.split(' '), function(klass, i) {
- activeClassName += (i > 0 ? ' ' : '') + klass + '-active';
+ var prefix = (i > 0 ? ' ' : '') + klass;
+ activeClassName += prefix + '-active';
+ pendingClassName += prefix + '-pending';
});
- element.addClass(activeClassName);
var eventCacheKey = elementData.cacheKey + ' ' + activeClassName;
var timings = getElementAnimationDetails(element, eventCacheKey);
-
var maxDuration = Math.max(timings.transitionDuration, timings.animationDuration);
if(maxDuration === 0) {
element.removeClass(activeClassName);
@@ -1491,8 +1604,6 @@ angular.module('ngAnimate', ['ng'])
}
var maxDelay = Math.max(timings.transitionDelay, timings.animationDelay);
- var stagger = elementData.stagger;
- var itemIndex = elementData.itemIndex;
var maxDelayTime = maxDelay * ONE_SECOND;
var style = '', appliedStyles = [];
@@ -1506,19 +1617,31 @@ angular.module('ngAnimate', ['ng'])
}
}
+ var itemIndex = elementData.itemIndex;
+ var stagger = elementData.stagger;
+
+ var staggerStyle, staggerTime = 0;
if(itemIndex > 0) {
+ var transitionStaggerDelay = 0;
if(stagger.transitionDelay > 0 && stagger.transitionDuration === 0) {
- var delayStyle = timings.transitionDelayStyle;
- style += CSS_PREFIX + 'transition-delay: ' +
- prepareStaggerDelay(delayStyle, stagger.transitionDelay, itemIndex) + '; ';
- appliedStyles.push(CSS_PREFIX + 'transition-delay');
+ transitionStaggerDelay = stagger.transitionDelay * itemIndex;
}
+ var animationStaggerDelay = 0;
if(stagger.animationDelay > 0 && stagger.animationDuration === 0) {
- style += CSS_PREFIX + 'animation-delay: ' +
- prepareStaggerDelay(timings.animationDelayStyle, stagger.animationDelay, itemIndex) + '; ';
- appliedStyles.push(CSS_PREFIX + 'animation-delay');
+ animationStaggerDelay = stagger.animationDelay * itemIndex;
+
+ staggerStyle = CSS_PREFIX + 'animation-play-state';
+ appliedStyles.push(staggerStyle);
+
+ style += staggerStyle + ':paused;';
}
+
+ staggerTime = Math.round(Math.max(transitionStaggerDelay, animationStaggerDelay) * 100) / 100;
+ }
+
+ if (!staggerTime) {
+ element.addClass(activeClassName);
}
if(appliedStyles.length > 0) {
@@ -1531,6 +1654,21 @@ angular.module('ngAnimate', ['ng'])
var startTime = Date.now();
var css3AnimationEvents = ANIMATIONEND_EVENT + ' ' + TRANSITIONEND_EVENT;
+ var animationTime = (maxDelay + maxDuration) * CLOSING_TIME_BUFFER;
+ var totalTime = (staggerTime + animationTime) * ONE_SECOND;
+
+ var staggerTimer;
+ if(staggerTime > 0) {
+ element.addClass(pendingClassName);
+ staggerTimer = $timeout(function() {
+ staggerTimer = null;
+ element.addClass(activeClassName);
+ element.removeClass(pendingClassName);
+ if (staggerStyle) {
+ element.css(staggerStyle, '');
+ }
+ }, staggerTime * ONE_SECOND, false);
+ }
element.on(css3AnimationEvents, onAnimationProgress);
elementData.closeAnimationFns.push(function() {
@@ -1538,10 +1676,6 @@ angular.module('ngAnimate', ['ng'])
activeAnimationComplete();
});
- var staggerTime = itemIndex * (Math.max(stagger.animationDelay, stagger.transitionDelay) || 0);
- var animationTime = (maxDelay + maxDuration) * CLOSING_TIME_BUFFER;
- var totalTime = (staggerTime + animationTime) * ONE_SECOND;
-
elementData.running++;
animationCloseHandler(element, totalTime);
return onEnd;
@@ -1552,6 +1686,10 @@ angular.module('ngAnimate', ['ng'])
function onEnd(cancelled) {
element.off(css3AnimationEvents, onAnimationProgress);
element.removeClass(activeClassName);
+ element.removeClass(pendingClassName);
+ if (staggerTimer) {
+ $timeout.cancel(staggerTimer);
+ }
animateClose(element, className);
var node = extractElementNode(element);
for (var i in appliedStyles) {
@@ -1581,15 +1719,6 @@ angular.module('ngAnimate', ['ng'])
}
}
- function prepareStaggerDelay(delayStyle, staggerDelay, index) {
- var style = '';
- forEach(delayStyle.split(','), function(val, i) {
- style += (i > 0 ? ',' : '') +
- (index * staggerDelay + parseInt(val, 10)) + 's';
- });
- return style;
- }
-
function animateBefore(animationEvent, element, className, calculationDecorator) {
if(animateSetup(animationEvent, element, className, calculationDecorator)) {
return function(cancelled) {
@@ -1708,7 +1837,7 @@ angular.module('ngAnimate', ['ng'])
function suffixClasses(classes, suffix) {
var className = '';
- classes = angular.isArray(classes) ? classes : classes.split(/\s+/);
+ classes = isArray(classes) ? classes : classes.split(/\s+/);
forEach(classes, function(klass, i) {
if(klass && klass.length > 0) {
className += (i > 0 ? ' ' : '') + klass + suffix;
diff --git a/src/ngMock/angular-mocks.js b/src/ngMock/angular-mocks.js
index e3be73835e4d..2f10af742672 100644
--- a/src/ngMock/angular-mocks.js
+++ b/src/ngMock/angular-mocks.js
@@ -767,14 +767,21 @@ angular.mock.animate = angular.module('ngAnimateMock', ['ng'])
};
});
- $provide.decorator('$animate', ['$delegate', '$$asyncCallback',
- function($delegate, $$asyncCallback) {
+ $provide.decorator('$animate', ['$delegate', '$$asyncCallback', '$timeout', '$browser',
+ function($delegate, $$asyncCallback, $timeout, $browser) {
var animate = {
queue : [],
enabled : $delegate.enabled,
- triggerCallbacks : function() {
+ triggerCallbackEvents : function() {
$$asyncCallback.flush();
},
+ triggerCallbackPromise : function() {
+ $timeout.flush(0);
+ },
+ triggerCallbacks : function() {
+ this.triggerCallbackEvents();
+ this.triggerCallbackPromise();
+ },
triggerReflow : function() {
angular.forEach(reflowQueue, function(fn) {
fn();
diff --git a/src/ngRoute/directive/ngView.js b/src/ngRoute/directive/ngView.js
index 02ae2aeb3c81..ba61a6cd1e54 100644
--- a/src/ngRoute/directive/ngView.js
+++ b/src/ngRoute/directive/ngView.js
@@ -206,7 +206,7 @@ function ngViewFactory( $route, $anchorScroll, $animate) {
currentScope = null;
}
if(currentElement) {
- $animate.leave(currentElement, function() {
+ $animate.leave(currentElement).then(function() {
previousElement = null;
});
previousElement = currentElement;
@@ -229,7 +229,7 @@ function ngViewFactory( $route, $anchorScroll, $animate) {
// function is called before linking the content, which would apply child
// directives to non existing elements.
var clone = $transclude(newScope, function(clone) {
- $animate.enter(clone, null, currentElement || $element, function onNgViewEnter () {
+ $animate.enter(clone, null, currentElement || $element).then(function onNgViewEnter () {
if (angular.isDefined(autoScrollExp)
&& (!autoScrollExp || scope.$eval(autoScrollExp))) {
$anchorScroll();
diff --git a/test/.jshintrc b/test/.jshintrc
index 8be371609fdb..b352fca2bcc8 100644
--- a/test/.jshintrc
+++ b/test/.jshintrc
@@ -51,6 +51,7 @@
"isBoolean": false,
"trim": false,
"isElement": false,
+ "isPromiseLike": false,
"makeMap": false,
"map": false,
"size": false,
diff --git a/test/helpers/matchers.js b/test/helpers/matchers.js
index 421feb061de8..2cb34c6872ab 100644
--- a/test/helpers/matchers.js
+++ b/test/helpers/matchers.js
@@ -50,6 +50,11 @@ beforeEach(function() {
toBePristine: cssMatcher('ng-pristine', 'ng-dirty'),
toBeUntouched: cssMatcher('ng-untouched', 'ng-touched'),
toBeTouched: cssMatcher('ng-touched', 'ng-untouched'),
+ toBeAPromise: function() {
+ this.message = valueFn(
+ "Expected object " + (this.isNot ? "not ": "") + "to be a promise");
+ return isPromiseLike(this.actual);
+ },
toBeShown: function() {
this.message = valueFn(
"Expected element " + (this.isNot ? "": "not ") + "to have 'ng-hide' class");
diff --git a/test/ng/animateSpec.js b/test/ng/animateSpec.js
index 7ab12b08f244..9e167dc35589 100644
--- a/test/ng/animateSpec.js
+++ b/test/ng/animateSpec.js
@@ -57,18 +57,18 @@ describe("$animate", function() {
expect(element).toBeHidden();
}));
- it("should run each method and return a noop function", inject(function($animate, $document) {
+ it("should run each method and return a promise", inject(function($animate, $document) {
var element = jqLite('');
var move = jqLite('');
var parent = jqLite($document[0].body);
parent.append(move);
- expect($animate.enter(element, parent)).toBe(noop);
- expect($animate.move(element, move)).toBe(noop);
- expect($animate.addClass(element, 'on')).toBe(noop);
- expect($animate.addClass(element, 'off')).toBe(noop);
- expect($animate.setClass(element, 'on', 'off')).toBe(noop);
- expect($animate.leave(element)).toBe(noop);
+ expect($animate.enter(element, parent)).toBeAPromise();
+ expect($animate.move(element, move)).toBeAPromise();
+ expect($animate.addClass(element, 'on')).toBeAPromise();
+ expect($animate.removeClass(element, 'off')).toBeAPromise();
+ expect($animate.setClass(element, 'on', 'off')).toBeAPromise();
+ expect($animate.leave(element)).toBeAPromise();
}));
it("should add and remove classes on SVG elements", inject(function($animate) {
diff --git a/test/ng/compileSpec.js b/test/ng/compileSpec.js
index 83e899345607..a27c2790f806 100755
--- a/test/ng/compileSpec.js
+++ b/test/ng/compileSpec.js
@@ -6019,9 +6019,12 @@ describe('$compile', function() {
$rootScope.$digest();
data = $animate.queue.shift();
- expect(data.event).toBe('setClass');
+ expect(data.event).toBe('addClass');
expect(data.args[1]).toBe('dice');
- expect(data.args[2]).toBe('rice');
+
+ data = $animate.queue.shift();
+ expect(data.event).toBe('removeClass');
+ expect(data.args[1]).toBe('rice');
expect(element.hasClass('ice')).toBe(true);
expect(element.hasClass('dice')).toBe(true);
diff --git a/test/ng/directive/ngClassSpec.js b/test/ng/directive/ngClassSpec.js
index e789d141440d..a00708f18d5d 100644
--- a/test/ng/directive/ngClassSpec.js
+++ b/test/ng/directive/ngClassSpec.js
@@ -391,7 +391,8 @@ describe('ngClass animations', function() {
$rootScope.val = 'two';
$rootScope.$digest();
- expect($animate.queue.shift().event).toBe('setClass');
+ expect($animate.queue.shift().event).toBe('addClass');
+ expect($animate.queue.shift().event).toBe('removeClass');
expect($animate.queue.length).toBe(0);
});
});
@@ -440,7 +441,7 @@ describe('ngClass animations', function() {
$compile(element)($rootScope);
var enterComplete = false;
- $animate.enter(element, $rootElement, null, function() {
+ $animate.enter(element, $rootElement, null).then(function() {
enterComplete = true;
});
@@ -506,9 +507,12 @@ describe('ngClass animations', function() {
$rootScope.$digest();
item = $animate.queue.shift();
- expect(item.event).toBe('setClass');
+ expect(item.event).toBe('addClass');
expect(item.args[1]).toBe('three');
- expect(item.args[2]).toBe('two');
+
+ item = $animate.queue.shift();
+ expect(item.event).toBe('removeClass');
+ expect(item.args[1]).toBe('two');
expect($animate.queue.length).toBe(0);
});
diff --git a/test/ng/directive/ngIfSpec.js b/test/ng/directive/ngIfSpec.js
index f952500ec90d..b4f421667657 100755
--- a/test/ng/directive/ngIfSpec.js
+++ b/test/ng/directive/ngIfSpec.js
@@ -315,14 +315,12 @@ describe('ngIf animations', function () {
it('should destroy the previous leave animation if a new one takes place', function() {
module(function($provide) {
- $provide.value('$animate', {
- enabled : function() { return true; },
- leave : function() {
- //DOM operation left blank
- },
- enter : function(element, parent) {
- parent.append(element);
- }
+ $provide.decorator('$animate', function($delegate, $$q) {
+ var emptyPromise = $$q.defer().promise;
+ $delegate.leave = function() {
+ return emptyPromise;
+ };
+ return $delegate;
});
});
inject(function ($compile, $rootScope, $animate) {
diff --git a/test/ng/directive/ngIncludeSpec.js b/test/ng/directive/ngIncludeSpec.js
index 828c6d2056fb..1d2f1ec4c89a 100644
--- a/test/ng/directive/ngIncludeSpec.js
+++ b/test/ng/directive/ngIncludeSpec.js
@@ -693,14 +693,12 @@ describe('ngInclude animations', function() {
it('should destroy the previous leave animation if a new one takes place', function() {
module(function($provide) {
- $provide.value('$animate', {
- enabled : function() { return true; },
- leave : function() {
- //DOM operation left blank
- },
- enter : function(element, parent, after) {
- angular.element(after).after(element);
- }
+ $provide.decorator('$animate', function($delegate, $$q) {
+ var emptyPromise = $$q.defer().promise;
+ $delegate.leave = function() {
+ return emptyPromise;
+ };
+ return $delegate;
});
});
inject(function ($compile, $rootScope, $animate, $templateCache) {
diff --git a/test/ng/directive/ngSwitchSpec.js b/test/ng/directive/ngSwitchSpec.js
index 5e48d68f19f3..f7b4e4422f00 100644
--- a/test/ng/directive/ngSwitchSpec.js
+++ b/test/ng/directive/ngSwitchSpec.js
@@ -397,14 +397,12 @@ describe('ngSwitch animations', function() {
it('should destroy the previous leave animation if a new one takes place', function() {
module(function($provide) {
- $provide.value('$animate', {
- enabled : function() { return true; },
- leave : function() {
- //DOM operation left blank
- },
- enter : function(element, parent, after) {
- angular.element(after).after(element);
- }
+ $provide.decorator('$animate', function($delegate, $$q) {
+ var emptyPromise = $$q.defer().promise;
+ $delegate.leave = function() {
+ return emptyPromise;
+ };
+ return $delegate;
});
});
inject(function ($compile, $rootScope, $animate, $templateCache) {
diff --git a/test/ngAnimate/animateSpec.js b/test/ngAnimate/animateSpec.js
index e674a08bfd63..505db6cbc1be 100644
--- a/test/ngAnimate/animateSpec.js
+++ b/test/ngAnimate/animateSpec.js
@@ -5,6 +5,17 @@ describe("ngAnimate", function() {
beforeEach(module('ngAnimate'));
beforeEach(module('ngAnimateMock'));
+ function getMaxValue(prop, element, $window) {
+ var node = element[0];
+ var cs = $window.getComputedStyle(node);
+ var prop0 = 'webkit' + prop.charAt(0).toUpperCase() + prop.substr(1);
+ var values = (cs[prop0] || cs[prop]).split(/\s*,\s*/);
+ var maxDelay = 0;
+ forEach(values, function(value) {
+ maxDelay = Math.max(parseFloat(value) || 0, maxDelay);
+ });
+ return maxDelay;
+ }
it("should disable animations on bootstrap for structural animations even after the first digest has passed", function() {
var hasBeenAnimated = false;
@@ -112,12 +123,14 @@ describe("ngAnimate", function() {
angular.element(document.body).append($rootElement);
$animate.addClass(elm1, 'klass');
+ $rootScope.$digest();
$animate.triggerReflow();
expect(count).toBe(1);
$animate.enabled(false);
$animate.addClass(elm1, 'klass2');
+ $rootScope.$digest();
$animate.triggerReflow();
expect(count).toBe(1);
@@ -126,18 +139,21 @@ describe("ngAnimate", function() {
elm1.append(elm2);
$animate.addClass(elm2, 'klass');
+ $rootScope.$digest();
$animate.triggerReflow();
expect(count).toBe(2);
$animate.enabled(false, elm1);
$animate.addClass(elm2, 'klass2');
+ $rootScope.$digest();
$animate.triggerReflow();
expect(count).toBe(2);
var root = angular.element($rootElement[0]);
$rootElement.addClass('animated');
$animate.addClass(root, 'klass2');
+ $rootScope.$digest();
$animate.triggerReflow();
expect(count).toBe(3);
});
@@ -162,6 +178,7 @@ describe("ngAnimate", function() {
var elm1 = $compile('')($rootScope);
$animate.addClass(elm1, 'klass2');
+ $rootScope.$digest();
expect(count).toBe(0);
});
});
@@ -193,6 +210,7 @@ describe("ngAnimate", function() {
expect(captured).toBe(false);
$animate.addClass(element, 'red');
+ $rootScope.$digest();
$animate.triggerReflow();
expect(captured).toBe(true);
@@ -200,6 +218,7 @@ describe("ngAnimate", function() {
$animate.enabled(false);
$animate.addClass(element, 'blue');
+ $rootScope.$digest();
$animate.triggerReflow();
expect(captured).toBe(false);
@@ -395,6 +414,7 @@ describe("ngAnimate", function() {
child.addClass('ng-hide');
expect(child).toBeHidden();
$animate.removeClass(child, 'ng-hide');
+ $rootScope.$digest();
if($sniffer.transitions) {
$animate.triggerReflow();
expect(child.hasClass('ng-hide-remove')).toBe(true);
@@ -412,6 +432,7 @@ describe("ngAnimate", function() {
$rootScope.$digest();
expect(child).toBeShown();
$animate.addClass(child, 'ng-hide');
+ $rootScope.$digest();
if($sniffer.transitions) {
$animate.triggerReflow();
expect(child.hasClass('ng-hide-add')).toBe(true);
@@ -450,6 +471,7 @@ describe("ngAnimate", function() {
inject(function($animate, $rootScope, $sniffer, $timeout) {
child.attr('class','classify no');
$animate.setClass(child, 'yes', 'no');
+ $rootScope.$digest();
$animate.triggerReflow();
expect(child.hasClass('yes')).toBe(true);
@@ -488,6 +510,7 @@ describe("ngAnimate", function() {
inject(function($animate, $rootScope, $sniffer, $timeout) {
child.attr('class','classify no');
$animate.setClass(child[0], 'yes', 'no');
+ $rootScope.$digest();
$animate.triggerReflow();
expect(child.hasClass('yes')).toBe(true);
@@ -529,6 +552,7 @@ describe("ngAnimate", function() {
inject(function($animate, $rootScope, $sniffer, $timeout) {
child.attr('class','classify no');
$animate.setClass(child, 'yes', 'no');
+ $rootScope.$digest();
$animate.triggerReflow();
expect(child.hasClass('yes')).toBe(true);
@@ -553,7 +577,7 @@ describe("ngAnimate", function() {
expect(child.attr('class')).toContain('ng-enter');
expect(child.attr('class')).toContain('ng-enter-active');
browserTrigger(child,'transitionend', { timeStamp: Date.now() + 1000, elapsedTime: 1 });
- $animate.triggerCallbacks();
+ $animate.triggerCallbackPromise();
//move
element.append(after);
@@ -564,10 +588,11 @@ describe("ngAnimate", function() {
expect(child.attr('class')).toContain('ng-move');
expect(child.attr('class')).toContain('ng-move-active');
browserTrigger(child,'transitionend', { timeStamp: Date.now() + 1000, elapsedTime: 1 });
- $animate.triggerCallbacks();
+ $animate.triggerCallbackPromise();
//hide
$animate.addClass(child, 'ng-hide');
+ $rootScope.$digest();
$animate.triggerReflow();
expect(child.attr('class')).toContain('ng-hide-add');
expect(child.attr('class')).toContain('ng-hide-add-active');
@@ -575,6 +600,7 @@ describe("ngAnimate", function() {
//show
$animate.removeClass(child, 'ng-hide');
+ $rootScope.$digest();
$animate.triggerReflow();
expect(child.attr('class')).toContain('ng-hide-remove');
expect(child.attr('class')).toContain('ng-hide-remove-active');
@@ -616,69 +642,72 @@ describe("ngAnimate", function() {
});
inject(function($animate, $sniffer, $rootScope, $timeout) {
- var fn;
+ var promise;
$animate.enabled(true);
$rootScope.$digest();
element[0].removeChild(child[0]);
child.addClass('track-me');
//enter
- fn = $animate.enter(child, element);
+ promise = $animate.enter(child, element);
$rootScope.$digest();
$animate.triggerReflow();
expect(captures.enter).toBeUndefined();
- fn();
+ promise.cancel();
expect(captures.enter).toBeTruthy();
- $animate.triggerCallbacks();
+ $animate.triggerCallbackPromise();
//move
element.append(after);
- fn = $animate.move(child, element, after);
+ promise = $animate.move(child, element, after);
$rootScope.$digest();
$animate.triggerReflow();
expect(captures.move).toBeUndefined();
- fn();
+ promise.cancel();
expect(captures.move).toBeTruthy();
- $animate.triggerCallbacks();
+ $animate.triggerCallbackPromise();
//addClass
- fn = $animate.addClass(child, 'ng-hide');
+ promise = $animate.addClass(child, 'ng-hide');
+ $rootScope.$digest();
$animate.triggerReflow();
expect(captures.addClass).toBeUndefined();
- fn();
+ promise.cancel();
expect(captures.addClass).toBeTruthy();
- $animate.triggerCallbacks();
+ $animate.triggerCallbackPromise();
//removeClass
- fn = $animate.removeClass(child, 'ng-hide');
+ promise = $animate.removeClass(child, 'ng-hide');
+ $rootScope.$digest();
$animate.triggerReflow();
expect(captures.removeClass).toBeUndefined();
- fn();
+ promise.cancel();
expect(captures.removeClass).toBeTruthy();
- $animate.triggerCallbacks();
+ $animate.triggerCallbackPromise();
//setClass
child.addClass('red');
- fn = $animate.setClass(child, 'blue', 'red');
+ promise = $animate.setClass(child, 'blue', 'red');
+ $rootScope.$digest();
$animate.triggerReflow();
expect(captures.setClass).toBeUndefined();
- fn();
+ promise.cancel();
expect(captures.setClass).toBeTruthy();
- $animate.triggerCallbacks();
+ $animate.triggerCallbackPromise();
//leave
- fn = $animate.leave(child);
+ promise = $animate.leave(child);
$rootScope.$digest();
expect(captures.leave).toBeUndefined();
- fn();
+ promise.cancel();
expect(captures.leave).toBeTruthy();
- $animate.triggerCallbacks();
+ $animate.triggerCallbackPromise();
});
});
@@ -695,12 +724,14 @@ describe("ngAnimate", function() {
element.text('123');
expect(element.text()).toBe('123');
$animate.removeClass(element, 'ng-hide');
+ $rootScope.$digest();
expect(element.text()).toBe('123');
$animate.enabled(true);
element.addClass('ng-hide');
$animate.removeClass(element, 'ng-hide');
+ $rootScope.$digest();
if($sniffer.transitions) {
$animate.triggerReflow();
}
@@ -716,6 +747,7 @@ describe("ngAnimate", function() {
expect(element).toBeShown();
$animate.addClass(child, 'ng-hide');
+ $rootScope.$digest();
if($sniffer.transitions) {
expect(child).toBeShown();
}
@@ -750,6 +782,8 @@ describe("ngAnimate", function() {
child.attr('style', 'width: 20px');
$animate.addClass(child, 'ng-hide');
+ $rootScope.$digest();
+
$animate.leave(child);
$rootScope.$digest();
@@ -772,6 +806,7 @@ describe("ngAnimate", function() {
child.addClass('custom-delay ng-hide');
$animate.removeClass(child, 'ng-hide');
+ $rootScope.$digest();
if($sniffer.transitions) {
$animate.triggerReflow();
browserTrigger(child,'transitionend', { timeStamp: Date.now() + 1000, elapsedTime: 1 });
@@ -805,12 +840,14 @@ describe("ngAnimate", function() {
inject(function($animate, $rootScope, $sniffer, $timeout) {
$animate.addClass(element, 'hide');
+ $rootScope.$digest();
expect(element).toHaveClass('ng-animate');
$animate.triggerReflow();
$animate.removeClass(element, 'hide');
+ $rootScope.$digest();
expect(addClassDoneSpy).toHaveBeenCalled();
$animate.triggerReflow();
@@ -818,7 +855,7 @@ describe("ngAnimate", function() {
expect(element).toHaveClass('ng-animate');
removeClassDone();
- $animate.triggerCallbacks();
+ $animate.triggerCallbackPromise();
expect(element).not.toHaveClass('ng-animate');
});
@@ -828,7 +865,7 @@ describe("ngAnimate", function() {
inject(function($animate, $rootScope, $sniffer, $timeout) {
var completed = false;
- $animate.enter(child, element, null, function() {
+ $animate.enter(child, element, null).then(function() {
completed = true;
});
$rootScope.$digest();
@@ -836,6 +873,7 @@ describe("ngAnimate", function() {
expect(completed).toBe(false);
$animate.addClass(child, 'green');
+ $rootScope.$digest();
expect(element.hasClass('green'));
expect(completed).toBe(false);
@@ -843,7 +881,7 @@ describe("ngAnimate", function() {
$animate.triggerReflow();
browserTrigger(child,'transitionend', { timeStamp: Date.now() + 1000, elapsedTime: 1 });
}
- $animate.triggerCallbacks();
+ $animate.triggerCallbackPromise();
expect(completed).toBe(true);
}));
@@ -865,6 +903,7 @@ describe("ngAnimate", function() {
$animate.enabled(false, element);
$animate.addClass(element, 'capture');
+ $rootScope.$digest();
expect(element.hasClass('capture')).toBe(true);
expect(capture).not.toBe(true);
});
@@ -876,7 +915,11 @@ describe("ngAnimate", function() {
element.append(child);
$animate.addClass(child, 'custom-delay');
+ $rootScope.$digest();
+
$animate.addClass(child, 'custom-long-delay');
+ $rootScope.$digest();
+
$animate.triggerReflow();
expect(child.hasClass('animation-cancelled')).toBe(false);
@@ -888,13 +931,17 @@ describe("ngAnimate", function() {
it("should NOT clobber all data on an element when animation is finished",
- inject(function($animate) {
+ inject(function($animate, $rootScope) {
child.css('display','none');
element.data('foo', 'bar');
$animate.removeClass(element, 'ng-hide');
+ $rootScope.$digest();
+
$animate.addClass(element, 'ng-hide');
+ $rootScope.$digest();
+
expect(element.data('foo')).toEqual('bar');
}));
@@ -903,6 +950,7 @@ describe("ngAnimate", function() {
inject(function($animate, $rootScope, $compile, $sniffer, $timeout) {
$animate.addClass(element, 'custom-delay custom-long-delay');
+ $rootScope.$digest();
$animate.triggerReflow();
$timeout.flush(2000);
$timeout.flush(20000);
@@ -926,6 +974,7 @@ describe("ngAnimate", function() {
$rootScope.$digest();
$animate.removeClass(element, 'ng-hide');
+ $rootScope.$digest();
if($sniffer.transitions) {
browserTrigger(element,'transitionend', { timeStamp: Date.now() + 1000, elapsedTime: 1 });
@@ -1004,6 +1053,7 @@ describe("ngAnimate", function() {
expect(element).toBeHidden();
$animate.removeClass(element, 'ng-hide');
+ $rootScope.$digest();
if ($sniffer.animations) {
$animate.triggerReflow();
browserTrigger(element,'animationend', { timeStamp: Date.now() + 4000, elapsedTime: 4 });
@@ -1029,6 +1079,7 @@ describe("ngAnimate", function() {
expect(element).toBeHidden();
$animate.removeClass(element, 'ng-hide');
+ $rootScope.$digest();
if ($sniffer.animations) {
$animate.triggerReflow();
browserTrigger(element,'animationend', { timeStamp: Date.now() + 6000, elapsedTime: 6 });
@@ -1056,6 +1107,7 @@ describe("ngAnimate", function() {
expect(element).toBeHidden();
$animate.removeClass(element, 'ng-hide');
+ $rootScope.$digest();
if ($sniffer.transitions) {
$animate.triggerReflow();
browserTrigger(element,'animationend', { timeStamp : Date.now() + 20000, elapsedTime: 10 });
@@ -1077,6 +1129,7 @@ describe("ngAnimate", function() {
element.addClass('ng-hide');
expect(element).toBeHidden();
$animate.removeClass(element, 'ng-hide');
+ $rootScope.$digest();
expect(element).toBeShown();
}));
@@ -1093,6 +1146,7 @@ describe("ngAnimate", function() {
element.addClass('custom');
$animate.removeClass(element, 'ng-hide');
+ $rootScope.$digest();
if($sniffer.animations) {
$animate.triggerReflow();
@@ -1102,6 +1156,8 @@ describe("ngAnimate", function() {
element.removeClass('ng-hide');
$animate.addClass(element, 'ng-hide');
+ $rootScope.$digest();
+
expect(element.hasClass('ng-hide-remove')).toBe(false); //added right away
if($sniffer.animations) { //cleanup some pending animations
@@ -1116,7 +1172,7 @@ describe("ngAnimate", function() {
);
- it("should stagger the items when the correct CSS class is provided",
+ it("should pause the playstate when performing a stagger animation",
inject(function($animate, $rootScope, $compile, $sniffer, $timeout, $document, $rootElement) {
if(!$sniffer.animations) return;
@@ -1153,10 +1209,9 @@ describe("ngAnimate", function() {
$animate.triggerReflow();
expect(elements[0].attr('style')).toBeFalsy();
- expect(elements[1].attr('style')).toMatch(/animation-delay: 0\.1\d*s/);
- expect(elements[2].attr('style')).toMatch(/animation-delay: 0\.2\d*s/);
- expect(elements[3].attr('style')).toMatch(/animation-delay: 0\.3\d*s/);
- expect(elements[4].attr('style')).toMatch(/animation-delay: 0\.4\d*s/);
+ for(i=1;i<5;i++) {
+ expect(elements[i].attr('style')).toMatch(/animation-play-state:\s*paused/);
+ }
//final closing timeout
$timeout.flush();
@@ -1175,10 +1230,9 @@ describe("ngAnimate", function() {
$timeout.verifyNoPendingTasks();
expect(elements[0].attr('style')).toBeFalsy();
- expect(elements[1].attr('style')).not.toMatch(/animation-delay: 0\.1\d*s/);
- expect(elements[2].attr('style')).not.toMatch(/animation-delay: 0\.2\d*s/);
- expect(elements[3].attr('style')).not.toMatch(/animation-delay: 0\.3\d*s/);
- expect(elements[4].attr('style')).not.toMatch(/animation-delay: 0\.4\d*s/);
+ for(i=1;i<5;i++) {
+ expect(elements[i].attr('style')).not.toMatch(/animation-play-state:\s*paused/);
+ }
}));
@@ -1210,23 +1264,28 @@ describe("ngAnimate", function() {
$rootScope.$digest();
expect(elements[0].attr('style')).toBeUndefined();
- expect(elements[1].attr('style')).toMatch(/animation:.*?none/);
- expect(elements[2].attr('style')).toMatch(/animation:.*?none/);
- expect(elements[3].attr('style')).toMatch(/animation:.*?none/);
+ for(i = 1; i < 4; i++) {
+ expect(elements[i].attr('style')).toMatch(/animation:.*?none/);
+ }
$animate.triggerReflow();
expect(elements[0].attr('style')).toBeUndefined();
- expect(elements[1].attr('style')).not.toMatch(/animation:.*?none/);
- expect(elements[1].attr('style')).toMatch(/animation-delay: 0.2\d*s/);
- expect(elements[2].attr('style')).not.toMatch(/animation:.*?none/);
- expect(elements[2].attr('style')).toMatch(/animation-delay: 0.4\d*s/);
- expect(elements[3].attr('style')).not.toMatch(/animation:.*?none/);
- expect(elements[3].attr('style')).toMatch(/animation-delay: 0.6\d*s/);
+ for(i = 1; i < 4; i++) {
+ expect(elements[i].attr('style')).not.toMatch(/animation:.*?none/);
+ expect(elements[i].attr('style')).toMatch(/animation-play-state:\s*paused/);
+ }
+
+ $timeout.flush(800);
+
+ for(i = 1; i < 4; i++) {
+ expect(elements[i].attr('style')).not.toMatch(/animation:.*?none/);
+ expect(elements[i].attr('style')).not.toMatch(/animation-play-state/);
+ }
}));
it("should stagger items when multiple animation durations/delays are defined",
- inject(function($animate, $rootScope, $compile, $sniffer, $timeout, $document, $rootElement) {
+ inject(function($animate, $rootScope, $compile, $sniffer, $timeout, $document, $rootElement, $window) {
if(!$sniffer.transitions) return;
@@ -1253,10 +1312,19 @@ describe("ngAnimate", function() {
$rootScope.$digest();
$animate.triggerReflow();
- expect(elements[0].attr('style')).toBeFalsy();
- expect(elements[1].attr('style')).toMatch(/animation-delay: 1\.1\d*s,\s*2\.1\d*s/);
- expect(elements[2].attr('style')).toMatch(/animation-delay: 1\.2\d*s,\s*2\.2\d*s/);
- expect(elements[3].attr('style')).toMatch(/animation-delay: 1\.3\d*s,\s*2\.3\d*s/);
+ for(i = 1; i < 4; i++) {
+ expect(elements[i]).not.toHaveClass('ng-enter-active');
+ expect(elements[i]).toHaveClass('ng-enter-pending');
+ expect(getMaxValue('animationDelay', elements[i], $window)).toBe(2);
+ }
+
+ $timeout.flush(300);
+
+ for(i = 1; i < 4; i++) {
+ expect(elements[i]).toHaveClass('ng-enter-active');
+ expect(elements[i]).not.toHaveClass('ng-enter-pending');
+ expect(getMaxValue('animationDelay', elements[i], $window)).toBe(2);
+ }
}));
});
@@ -1279,6 +1347,7 @@ describe("ngAnimate", function() {
element.addClass('ng-hide');
expect(element).toBeHidden();
$animate.removeClass(element, 'ng-hide');
+ $rootScope.$digest();
expect(element).toBeShown();
$animate.enabled(true);
@@ -1287,6 +1356,7 @@ describe("ngAnimate", function() {
expect(element).toBeHidden();
$animate.removeClass(element, 'ng-hide');
+ $rootScope.$digest();
if ($sniffer.transitions) {
$animate.triggerReflow();
browserTrigger(element,'transitionend', { timeStamp: Date.now() + 1000, elapsedTime: 1 });
@@ -1310,6 +1380,7 @@ describe("ngAnimate", function() {
element.addClass('ng-hide');
$animate.removeClass(element, 'ng-hide');
+ $rootScope.$digest();
if ($sniffer.transitions) {
$animate.triggerReflow();
@@ -1340,6 +1411,8 @@ describe("ngAnimate", function() {
element.addClass('ng-hide');
$animate.removeClass(element, 'ng-hide');
+ $rootScope.$digest();
+
expect(element).toBeShown();
$animate.enabled(true);
@@ -1347,6 +1420,7 @@ describe("ngAnimate", function() {
expect(element).toBeHidden();
$animate.removeClass(element, 'ng-hide');
+ $rootScope.$digest();
if ($sniffer.transitions) {
$animate.triggerReflow();
var now = Date.now();
@@ -1376,6 +1450,7 @@ describe("ngAnimate", function() {
element.addClass('ng-hide');
$animate.removeClass(element, 'ng-hide');
+ $rootScope.$digest();
$animate.triggerReflow();
@@ -1399,6 +1474,7 @@ describe("ngAnimate", function() {
ss.addRule('.on', style);
element = $compile(html(''))($rootScope);
$animate.addClass(element, 'on');
+ $rootScope.$digest();
$animate.triggerReflow();
@@ -1424,6 +1500,7 @@ describe("ngAnimate", function() {
expect(element).toBeHidden();
$animate.removeClass(element, 'ng-hide');
+ $rootScope.$digest();
if ($sniffer.transitions) {
$animate.triggerReflow();
}
@@ -1449,6 +1526,7 @@ describe("ngAnimate", function() {
element.addClass('ng-hide');
$animate.removeClass(element, 'ng-hide');
+ $rootScope.$digest();
if($sniffer.transitions) {
$animate.triggerReflow();
@@ -1461,6 +1539,7 @@ describe("ngAnimate", function() {
expect(element).toBeShown();
$animate.addClass(element, 'ng-hide');
+ $rootScope.$digest();
if($sniffer.transitions) {
$animate.triggerReflow();
@@ -1505,12 +1584,14 @@ describe("ngAnimate", function() {
element = $compile(html('1
'))($rootScope);
$animate.addClass(element, 'my-class');
+ $rootScope.$digest();
expect(element.attr('style')).not.toMatch(/transition.*?:\s*none/);
expect(element.hasClass('my-class')).toBe(false);
expect(element.hasClass('my-class-add')).toBe(true);
$animate.triggerReflow();
+ $rootScope.$digest();
expect(element.attr('style')).not.toMatch(/transition.*?:\s*none/);
expect(element.hasClass('my-class')).toBe(true);
@@ -1519,7 +1600,7 @@ describe("ngAnimate", function() {
}));
it("should stagger the items when the correct CSS class is provided",
- inject(function($animate, $rootScope, $compile, $sniffer, $timeout, $document, $rootElement) {
+ inject(function($animate, $rootScope, $compile, $sniffer, $timeout, $document, $rootElement, $browser) {
if(!$sniffer.transitions) return;
@@ -1554,11 +1635,8 @@ describe("ngAnimate", function() {
$rootScope.$digest();
$animate.triggerReflow();
- expect(elements[0].attr('style')).toBeFalsy();
- expect(elements[1].attr('style')).toMatch(/transition-delay: 0\.1\d*s/);
- expect(elements[2].attr('style')).toMatch(/transition-delay: 0\.2\d*s/);
- expect(elements[3].attr('style')).toMatch(/transition-delay: 0\.3\d*s/);
- expect(elements[4].attr('style')).toMatch(/transition-delay: 0\.4\d*s/);
+ expect($browser.deferredFns.length).toEqual(5); //4 staggers + 1 combined timeout
+ $timeout.flush();
for(i = 0; i < 5; i++) {
dealoc(elements[i]);
@@ -1571,16 +1649,12 @@ describe("ngAnimate", function() {
$rootScope.$digest();
$animate.triggerReflow();
- expect(elements[0].attr('style')).toBeFalsy();
- expect(elements[1].attr('style')).not.toMatch(/transition-delay: 0\.1\d*s/);
- expect(elements[2].attr('style')).not.toMatch(/transition-delay: 0\.2\d*s/);
- expect(elements[3].attr('style')).not.toMatch(/transition-delay: 0\.3\d*s/);
- expect(elements[4].attr('style')).not.toMatch(/transition-delay: 0\.4\d*s/);
+ expect($browser.deferredFns.length).toEqual(0); //no animation was triggered
}));
it("should stagger items when multiple transition durations/delays are defined",
- inject(function($animate, $rootScope, $compile, $sniffer, $timeout, $document, $rootElement) {
+ inject(function($animate, $rootScope, $compile, $sniffer, $timeout, $document, $rootElement, $window) {
if(!$sniffer.transitions) return;
@@ -1607,11 +1681,19 @@ describe("ngAnimate", function() {
$rootScope.$digest();
$animate.triggerReflow();
- expect(elements[0].attr('style')).toMatch(/transition-duration: 1\d*s,\s*3\d*s;/);
- expect(elements[0].attr('style')).not.toContain('transition-delay');
- expect(elements[1].attr('style')).toMatch(/transition-delay: 2\.1\d*s,\s*4\.1\d*s/);
- expect(elements[2].attr('style')).toMatch(/transition-delay: 2\.2\d*s,\s*4\.2\d*s/);
- expect(elements[3].attr('style')).toMatch(/transition-delay: 2\.3\d*s,\s*4\.3\d*s/);
+ for(i = 1; i < 4; i++) {
+ expect(elements[i]).not.toHaveClass('ng-enter-active');
+ expect(elements[i]).toHaveClass('ng-enter-pending');
+ expect(getMaxValue('transitionDelay', elements[i], $window)).toBe(4);
+ }
+
+ $timeout.flush(300);
+
+ for(i = 1; i < 4; i++) {
+ expect(elements[i]).toHaveClass('ng-enter-active');
+ expect(elements[i]).not.toHaveClass('ng-enter-pending');
+ expect(getMaxValue('transitionDelay', elements[i], $window)).toBe(4);
+ }
}));
@@ -1626,6 +1708,7 @@ describe("ngAnimate", function() {
element = $compile(html('foo
'))($rootScope);
$animate.addClass(element, 'some-class');
+ $rootScope.$digest();
$animate.triggerReflow(); //reflow
expect(element.hasClass('some-class-add-active')).toBe(true);
@@ -1752,20 +1835,22 @@ describe("ngAnimate", function() {
$animate.triggerReflow(); //reflow
expect(element.children().length).toBe(5);
- for(i = 0; i < 5; i++) {
- expect(kids[i].hasClass('ng-enter-active')).toBe(true);
+ for(i = 1; i < 5; i++) {
+ expect(kids[i]).not.toHaveClass('ng-enter-active');
+ expect(kids[i]).toHaveClass('ng-enter-pending');
}
- $timeout.flush(7500);
+ $timeout.flush(2000);
- for(i = 0; i < 5; i++) {
- expect(kids[i].hasClass('ng-enter-active')).toBe(true);
+ for(i = 1; i < 5; i++) {
+ expect(kids[i]).toHaveClass('ng-enter-active');
+ expect(kids[i]).not.toHaveClass('ng-enter-pending');
}
//(stagger * index) + (duration + delay) * 150%
//0.5 * 4 + 5 * 1.5 = 9500;
- //9500 - 7500 = 2000
- $timeout.flush(1999); //remove 1999 more
+ //9500 - 2000 - 7499 = 1
+ $timeout.flush(7499);
for(i = 0; i < 5; i++) {
expect(kids[i].hasClass('ng-enter-active')).toBe(true);
@@ -1778,6 +1863,52 @@ describe("ngAnimate", function() {
}
}));
+ it("should cancel all the existing stagger timers when the animation is cancelled",
+ inject(function($animate, $rootScope, $compile, $sniffer, $timeout, $browser) {
+
+ if (!$sniffer.transitions) return;
+
+ ss.addRule('.entering-element.ng-enter',
+ '-webkit-transition:5s linear all;' +
+ 'transition:5s linear all;');
+
+ ss.addRule('.entering-element.ng-enter-stagger',
+ '-webkit-transition-delay:1s;' +
+ 'transition-delay:1s;');
+
+ var cancellations = [];
+ element = $compile(html(''))($rootScope);
+ var kids = [];
+ for(var i = 0; i < 5; i++) {
+ kids.push(angular.element(''));
+ cancellations.push($animate.enter(kids[i], element).cancel);
+ }
+ $rootScope.$digest();
+
+ $animate.triggerReflow(); //reflow
+ expect(element.children().length).toBe(5);
+
+ for(i = 1; i < 5; i++) {
+ expect(kids[i]).not.toHaveClass('ng-enter-active');
+ expect(kids[i]).toHaveClass('ng-enter-pending');
+ }
+
+ expect($browser.deferredFns.length).toEqual(5); //4 staggers + 1 combined timeout
+
+ forEach(cancellations, function(fn) {
+ fn();
+ });
+
+ for(i = 1; i < 5; i++) {
+ expect(kids[i]).not.toHaveClass('ng-enter');
+ expect(kids[i]).not.toHaveClass('ng-enter-active');
+ expect(kids[i]).not.toHaveClass('ng-enter-pending');
+ }
+
+ //the staggers are gone, but the global timeout remains
+ expect($browser.deferredFns.length).toEqual(1);
+ }));
+
it("should not allow the closing animation to close off a successive animation midway",
inject(function($animate, $rootScope, $compile, $sniffer, $timeout) {
@@ -1792,11 +1923,13 @@ describe("ngAnimate", function() {
element = $compile(html('foo
'))($rootScope);
$animate.addClass(element, 'some-class');
+ $rootScope.$digest();
$animate.triggerReflow(); //reflow
expect(element.hasClass('some-class-add-active')).toBe(true);
$animate.removeClass(element, 'some-class');
+ $rootScope.$digest();
$animate.triggerReflow(); //second reflow
@@ -1812,7 +1945,7 @@ describe("ngAnimate", function() {
it("should apply staggering to both transitions and keyframe animations when used within the same animation",
- inject(function($animate, $rootScope, $compile, $sniffer, $timeout, $document, $rootElement) {
+ inject(function($animate, $rootScope, $compile, $sniffer, $timeout, $document, $rootElement, $browser) {
if(!$sniffer.transitions) return;
@@ -1842,14 +1975,23 @@ describe("ngAnimate", function() {
$rootScope.$digest();
$animate.triggerReflow();
+ expect($browser.deferredFns.length).toEqual(3); //2 staggers + 1 combined timeout
expect(elements[0].attr('style')).toBeFalsy();
+ expect(elements[1].attr('style')).toMatch(/animation-play-state:\s*paused/);
+ expect(elements[2].attr('style')).toMatch(/animation-play-state:\s*paused/);
- expect(elements[1].attr('style')).toMatch(/transition-delay:\s+1.1\d*/);
- expect(elements[1].attr('style')).toMatch(/animation-delay: 1\.2\d*s,\s*2\.2\d*s/);
+ for(i = 1; i < 3; i++) {
+ expect(elements[i]).not.toHaveClass('ng-enter-active');
+ expect(elements[i]).toHaveClass('ng-enter-pending');
+ }
- expect(elements[2].attr('style')).toMatch(/transition-delay:\s+1.2\d*/);
- expect(elements[2].attr('style')).toMatch(/animation-delay: 1\.4\d*s,\s*2\.4\d*s/);
+ $timeout.flush(0.4 * 1000);
+
+ for(i = 1; i < 3; i++) {
+ expect(elements[i]).toHaveClass('ng-enter-active');
+ expect(elements[i]).not.toHaveClass('ng-enter-pending');
+ }
for(i = 0; i < 3; i++) {
browserTrigger(elements[i],'transitionend', { timeStamp: Date.now() + 22000, elapsedTime: 22000 });
@@ -1884,7 +2026,7 @@ describe("ngAnimate", function() {
expect(element.hasClass('ng-enter')).toBe(true);
expect(element.hasClass('ng-enter-active')).toBe(true);
browserTrigger(element,'transitionend', { timeStamp: Date.now() + 22000, elapsedTime: 22 });
- $animate.triggerCallbacks();
+ $animate.triggerCallbackPromise();
}
expect(element.hasClass('abc')).toBe(true);
@@ -1898,7 +2040,7 @@ describe("ngAnimate", function() {
expect(element.hasClass('ng-enter')).toBe(true);
expect(element.hasClass('ng-enter-active')).toBe(true);
browserTrigger(element,'transitionend', { timeStamp: Date.now() + 11000, elapsedTime: 11 });
- $animate.triggerCallbacks();
+ $animate.triggerCallbackPromise();
}
expect(element.hasClass('xyz')).toBe(true);
}));
@@ -1968,12 +2110,12 @@ describe("ngAnimate", function() {
body.append($rootElement);
var flag = false;
- $animate.enter(element, parent, null, function() {
+ $animate.enter(element, parent, null).then(function() {
flag = true;
});
$rootScope.$digest();
- $animate.triggerCallbacks();
+ $animate.triggerCallbackPromise();
expect(flag).toBe(true);
}));
@@ -1988,12 +2130,12 @@ describe("ngAnimate", function() {
body.append($rootElement);
var flag = false;
- $animate.leave(element, function() {
+ $animate.leave(element).then(function() {
flag = true;
});
$rootScope.$digest();
- $animate.triggerCallbacks();
+ $animate.triggerCallbackPromise();
expect(flag).toBe(true);
}));
@@ -2009,12 +2151,12 @@ describe("ngAnimate", function() {
body.append($rootElement);
var flag = false;
- $animate.move(element, parent, parent2, function() {
+ $animate.move(element, parent, parent2).then(function() {
flag = true;
});
$rootScope.$digest();
- $animate.triggerCallbacks();
+ $animate.triggerCallbackPromise();
expect(flag).toBe(true);
expect(element.parent().id).toBe(parent2.id);
@@ -2032,17 +2174,19 @@ describe("ngAnimate", function() {
body.append($rootElement);
var signature = '';
- $animate.addClass(element, 'on', function() {
+ $animate.addClass(element, 'on').then(function() {
signature += 'A';
});
+ $rootScope.$digest();
$animate.triggerReflow();
- $animate.removeClass(element, 'on', function() {
+ $animate.removeClass(element, 'on').then(function() {
signature += 'B';
});
+ $rootScope.$digest();
$animate.triggerReflow();
- $animate.triggerCallbacks();
+ $animate.triggerCallbackPromise();
expect(signature).toBe('AB');
}));
@@ -2059,12 +2203,13 @@ describe("ngAnimate", function() {
expect(element.hasClass('off')).toBe(true);
var signature = '';
- $animate.setClass(element, 'on', 'off', function() {
+ $animate.setClass(element, 'on', 'off').then(function() {
signature += 'Z';
});
+ $rootScope.$digest();
$animate.triggerReflow();
- $animate.triggerCallbacks();
+ $animate.triggerCallbackPromise();
expect(signature).toBe('Z');
expect(element.hasClass('on')).toBe(true);
@@ -2098,26 +2243,29 @@ describe("ngAnimate", function() {
steps.push(['close', data.className, data.event]);
});
- $animate.addClass(element, 'klass', function() {
+ $animate.addClass(element, 'klass').then(function() {
steps.push(['done', 'klass', 'addClass']);
});
+ $rootScope.$digest();
- $animate.triggerCallbacks();
+ $animate.triggerCallbackEvents();
expect(steps.pop()).toEqual(['before', 'klass', 'addClass']);
$animate.triggerReflow();
- $animate.triggerCallbacks();
+ $animate.triggerCallbackEvents();
expect(steps.pop()).toEqual(['after', 'klass', 'addClass']);
browserTrigger(element,'transitionend', { timeStamp: Date.now() + 1000, elapsedTime: 1 });
- $animate.triggerCallbacks();
+ $animate.triggerCallbackEvents();
expect(steps.shift()).toEqual(['close', 'klass', 'addClass']);
+ $animate.triggerCallbackPromise();
+
expect(steps.shift()).toEqual(['done', 'klass', 'addClass']);
}));
@@ -2143,7 +2291,7 @@ describe("ngAnimate", function() {
$animate.enter(element, parent);
$rootScope.$digest();
- $animate.triggerCallbacks();
+ $animate.triggerCallbackEvents();
expect(steps.shift()).toEqual(['before', 'ng-enter', 'enter']);
expect(steps.shift()).toEqual(['after', 'ng-enter', 'enter']);
@@ -2173,11 +2321,12 @@ describe("ngAnimate", function() {
body.append($rootElement);
var flag = false;
- $animate.removeClass(element, 'ng-hide', function() {
+ $animate.removeClass(element, 'ng-hide').then(function() {
flag = true;
});
+ $rootScope.$digest();
- $animate.triggerCallbacks();
+ $animate.triggerCallbackPromise();
expect(flag).toBe(true);
}));
@@ -2196,15 +2345,16 @@ describe("ngAnimate", function() {
var element = parent.find('span');
var flag = false;
- $animate.addClass(element, 'ng-hide', function() {
+ $animate.addClass(element, 'ng-hide').then(function() {
flag = true;
});
+ $rootScope.$digest();
if($sniffer.transitions) {
$animate.triggerReflow();
browserTrigger(element,'transitionend', { timeStamp: Date.now() + 1000, elapsedTime: 1 });
}
- $animate.triggerCallbacks();
+ $animate.triggerCallbackPromise();
expect(flag).toBe(true);
}));
@@ -2219,11 +2369,12 @@ describe("ngAnimate", function() {
element.addClass('custom');
var flag = false;
- $animate.removeClass(element, 'ng-hide', function() {
+ $animate.removeClass(element, 'ng-hide').then(function() {
flag = true;
});
+ $rootScope.$digest();
- $animate.triggerCallbacks();
+ $animate.triggerCallbackPromise();
expect(flag).toBe(true);
}));
@@ -2242,19 +2393,21 @@ describe("ngAnimate", function() {
var element = parent.find('span');
var signature = '';
- $animate.removeClass(element, 'ng-hide', function() {
+ $animate.removeClass(element, 'ng-hide').then(function() {
signature += 'A';
});
- $animate.addClass(element, 'ng-hide', function() {
+ $rootScope.$digest();
+ $animate.addClass(element, 'ng-hide').then(function() {
signature += 'B';
});
+ $rootScope.$digest();
$animate.addClass(element, 'ng-hide'); //earlier animation cancelled
if($sniffer.transitions) {
$animate.triggerReflow();
browserTrigger(element,'transitionend', { timeStamp: Date.now() + 1000, elapsedTime: 9 });
}
- $animate.triggerCallbacks();
+ $animate.triggerCallbackPromise();
expect(signature).toBe('AB');
}));
});
@@ -2292,6 +2445,7 @@ describe("ngAnimate", function() {
//skipped animations
captured = 'none';
$animate.removeClass(element, 'some-class');
+ $rootScope.$digest();
expect(element.hasClass('some-class')).toBe(false);
expect(captured).toBe('none');
@@ -2299,18 +2453,21 @@ describe("ngAnimate", function() {
captured = 'nothing';
$animate.addClass(element, 'some-class');
+ $rootScope.$digest();
expect(captured).toBe('nothing');
expect(element.hasClass('some-class')).toBe(true);
//actual animations
captured = 'none';
$animate.removeClass(element, 'some-class');
+ $rootScope.$digest();
$animate.triggerReflow();
expect(element.hasClass('some-class')).toBe(false);
expect(captured).toBe('removeClass-some-class');
captured = 'nothing';
$animate.addClass(element, 'some-class');
+ $rootScope.$digest();
$animate.triggerReflow();
expect(element.hasClass('some-class')).toBe(true);
expect(captured).toBe('addClass-some-class');
@@ -2326,6 +2483,7 @@ describe("ngAnimate", function() {
//skipped animations
captured = 'none';
$animate.removeClass(element[0], 'some-class');
+ $rootScope.$digest();
expect(element.hasClass('some-class')).toBe(false);
expect(captured).toBe('none');
@@ -2333,18 +2491,21 @@ describe("ngAnimate", function() {
captured = 'nothing';
$animate.addClass(element[0], 'some-class');
+ $rootScope.$digest();
expect(captured).toBe('nothing');
expect(element.hasClass('some-class')).toBe(true);
//actual animations
captured = 'none';
$animate.removeClass(element[0], 'some-class');
+ $rootScope.$digest();
$animate.triggerReflow();
expect(element.hasClass('some-class')).toBe(false);
expect(captured).toBe('removeClass-some-class');
captured = 'nothing';
$animate.addClass(element[0], 'some-class');
+ $rootScope.$digest();
$animate.triggerReflow();
expect(element.hasClass('some-class')).toBe(true);
expect(captured).toBe('addClass-some-class');
@@ -2359,11 +2520,13 @@ describe("ngAnimate", function() {
var element = jqLite(parent.find('span'));
$animate.addClass(element,'klass');
+ $rootScope.$digest();
$animate.triggerReflow();
expect(element.hasClass('klass')).toBe(true);
$animate.removeClass(element,'klass');
+ $rootScope.$digest();
$animate.triggerReflow();
expect(element.hasClass('klass')).toBe(false);
@@ -2382,19 +2545,21 @@ describe("ngAnimate", function() {
var signature = '';
- $animate.addClass(element,'klass', function() {
+ $animate.addClass(element,'klass').then(function() {
signature += 'A';
});
+ $rootScope.$digest();
$animate.triggerReflow();
expect(element.hasClass('klass')).toBe(true);
- $animate.removeClass(element,'klass', function() {
+ $animate.removeClass(element,'klass').then(function() {
signature += 'B';
});
+ $rootScope.$digest();
$animate.triggerReflow();
- $animate.triggerCallbacks();
+ $animate.triggerCallbackPromise();
expect(element.hasClass('klass')).toBe(false);
expect(signature).toBe('AB');
}));
@@ -2415,9 +2580,10 @@ describe("ngAnimate", function() {
var signature = '';
- $animate.addClass(element,'klass', function() {
+ $animate.addClass(element,'klass').then(function() {
signature += '1';
});
+ $rootScope.$digest();
if($sniffer.transitions) {
expect(element.hasClass('klass-add')).toBe(true);
@@ -2427,12 +2593,13 @@ describe("ngAnimate", function() {
browserTrigger(element,'transitionend', { timeStamp: Date.now() + 3000, elapsedTime: 3 });
}
- $animate.triggerCallbacks();
+ $animate.triggerCallbackPromise();
//this cancels out the older animation
- $animate.removeClass(element,'klass', function() {
+ $animate.removeClass(element,'klass').then(function() {
signature += '2';
});
+ $rootScope.$digest();
if($sniffer.transitions) {
expect(element.hasClass('klass-remove')).toBe(true);
@@ -2445,7 +2612,7 @@ describe("ngAnimate", function() {
browserTrigger(element,'transitionend', { timeStamp: Date.now() + 3000, elapsedTime: 3 });
}
- $animate.triggerCallbacks();
+ $animate.triggerCallbackPromise();
expect(element.hasClass('klass')).toBe(false);
expect(signature).toBe('12');
@@ -2462,25 +2629,27 @@ describe("ngAnimate", function() {
var signature = '';
- $animate.addClass(element,'klassy', function() {
+ $animate.addClass(element,'klassy').then(function() {
signature += 'X';
});
+ $rootScope.$digest();
$animate.triggerReflow();
$timeout.flush(500);
expect(element.hasClass('klassy')).toBe(true);
- $animate.removeClass(element,'klassy', function() {
+ $animate.removeClass(element,'klassy').then(function() {
signature += 'Y';
});
+ $rootScope.$digest();
$animate.triggerReflow();
$timeout.flush(3000);
expect(element.hasClass('klassy')).toBe(false);
- $animate.triggerCallbacks();
+ $animate.triggerCallbackPromise();
expect(signature).toBe('XY');
}));
@@ -2494,25 +2663,27 @@ describe("ngAnimate", function() {
var signature = '';
- $animate.addClass(element[0],'klassy', function() {
+ $animate.addClass(element[0],'klassy').then(function() {
signature += 'X';
});
+ $rootScope.$digest();
$animate.triggerReflow();
$timeout.flush(500);
expect(element.hasClass('klassy')).toBe(true);
- $animate.removeClass(element[0],'klassy', function() {
+ $animate.removeClass(element[0],'klassy').then(function() {
signature += 'Y';
});
+ $rootScope.$digest();
$animate.triggerReflow();
$timeout.flush(3000);
expect(element.hasClass('klassy')).toBe(false);
- $animate.triggerCallbacks();
+ $animate.triggerCallbackPromise();
expect(signature).toBe('XY');
}));
@@ -2531,9 +2702,10 @@ describe("ngAnimate", function() {
var signature = '';
- $animate.addClass(element,'klass', function() {
+ $animate.addClass(element,'klass').then(function() {
signature += 'd';
});
+ $rootScope.$digest();
if($sniffer.transitions) {
$animate.triggerReflow();
@@ -2544,12 +2716,13 @@ describe("ngAnimate", function() {
expect(element.hasClass('klass-add-active')).toBe(false);
}
- $animate.triggerCallbacks();
+ $animate.triggerCallbackPromise();
expect(element.hasClass('klass')).toBe(true);
- $animate.removeClass(element,'klass', function() {
+ $animate.removeClass(element,'klass').then(function() {
signature += 'b';
});
+ $rootScope.$digest();
if($sniffer.transitions) {
$animate.triggerReflow();
@@ -2560,7 +2733,7 @@ describe("ngAnimate", function() {
expect(element.hasClass('klass-remove-active')).toBe(false);
}
- $animate.triggerCallbacks();
+ $animate.triggerCallbackPromise();
expect(element.hasClass('klass')).toBe(false);
expect(signature).toBe('db');
@@ -2581,10 +2754,12 @@ describe("ngAnimate", function() {
var element = jqLite(parent.find('span'));
var flag = false;
- $animate.addClass(element,'one two', function() {
+ $animate.addClass(element,'one two').then(function() {
flag = true;
});
+ $rootScope.$digest();
+
if($sniffer.transitions) {
$animate.triggerReflow();
expect(element.hasClass('one-add')).toBe(true);
@@ -2600,7 +2775,7 @@ describe("ngAnimate", function() {
expect(element.hasClass('two-add-active')).toBe(false);
}
- $animate.triggerCallbacks();
+ $animate.triggerCallbackPromise();
expect(element.hasClass('one')).toBe(true);
expect(element.hasClass('two')).toBe(true);
@@ -2627,9 +2802,10 @@ describe("ngAnimate", function() {
expect(element.hasClass('two')).toBe(true);
var flag = false;
- $animate.removeClass(element,'one two', function() {
+ $animate.removeClass(element,'one two').then(function() {
flag = true;
});
+ $rootScope.$digest();
if($sniffer.transitions) {
$animate.triggerReflow();
@@ -2646,7 +2822,7 @@ describe("ngAnimate", function() {
expect(element.hasClass('two-remove-active')).toBe(false);
}
- $animate.triggerCallbacks();
+ $animate.triggerCallbackPromise();
expect(element.hasClass('one')).toBe(false);
expect(element.hasClass('two')).toBe(false);
@@ -2808,7 +2984,7 @@ describe("ngAnimate", function() {
child.addClass('usurper');
$animate.leave(child);
$rootScope.$digest();
- $animate.triggerCallbacks();
+ $animate.triggerCallbackPromise();
expect(child.hasClass('ng-enter')).toBe(false);
expect(child.hasClass('ng-enter-active')).toBe(false);
@@ -2862,10 +3038,10 @@ describe("ngAnimate", function() {
// if($sniffer.transitions) {
// expect(element.hasClass('on')).toBe(false);
// expect(element.hasClass('on-add')).toBe(true);
- // $animate.triggerCallbacks();
+ // $animate.triggerCallbackPromise();
// }
//
- // $animate.triggerCallbacks();
+ // $animate.triggerCallbackPromise();
//
// expect(element.hasClass('on')).toBe(true);
// expect(element.hasClass('on-add')).toBe(false);
@@ -2878,7 +3054,7 @@ describe("ngAnimate", function() {
// $timeout.flush(10000);
// }
//
- // $animate.triggerCallbacks();
+ // $animate.triggerCallbackPromise();
// expect(element.hasClass('on')).toBe(false);
// expect(element.hasClass('on-remove')).toBe(false);
// expect(element.hasClass('on-remove-active')).toBe(false);
@@ -2921,11 +3097,11 @@ describe("ngAnimate", function() {
//
// if($sniffer.transitions) {
// expect(element).toBeShown(); //still showing
- // $animate.triggerCallbacks();
+ // $animate.triggerCallbackPromise();
// expect(element).toBeShown();
// $timeout.flush(5555);
// }
- // $animate.triggerCallbacks();
+ // $animate.triggerCallbackPromise();
// expect(element).toBeHidden();
//
// expect(element.hasClass('showing')).toBe(false);
@@ -2934,11 +3110,11 @@ describe("ngAnimate", function() {
//
// if($sniffer.transitions) {
// expect(element).toBeHidden();
- // $animate.triggerCallbacks();
+ // $animate.triggerCallbackPromise();
// expect(element).toBeHidden();
// $timeout.flush(5580);
// }
- // $animate.triggerCallbacks();
+ // $animate.triggerCallbackPromise();
// expect(element).toBeShown();
//
// expect(element.hasClass('showing')).toBe(true);
@@ -3004,14 +3180,17 @@ describe("ngAnimate", function() {
var element = html($compile('')($rootScope));
$animate.addClass(element, 'super');
+ $rootScope.$digest();
$animate.triggerReflow();
expect(element.data('classify')).toBe('add-super');
$animate.removeClass(element, 'super');
+ $rootScope.$digest();
$animate.triggerReflow();
expect(element.data('classify')).toBe('remove-super');
$animate.addClass(element, 'superguy');
+ $rootScope.$digest();
$animate.triggerReflow();
expect(element.data('classify')).toBe('add-superguy');
});
@@ -3139,6 +3318,7 @@ describe("ngAnimate", function() {
$animate.triggerCallbacks();
$animate.addClass(child, 'something');
+ $rootScope.$digest();
if($sniffer.transitions) {
$animate.triggerReflow();
}
@@ -3155,8 +3335,115 @@ describe("ngAnimate", function() {
expect(child.hasClass('something-add-active')).toBe(false);
}
});
+
});
+ it('should coalesce all class-based animation calls together into a single animation', function() {
+ var log = [];
+ var track = function(name) {
+ return function() {
+ log.push({ name : name, className : arguments[1] });
+ };
+ };
+ module(function($animateProvider) {
+ $animateProvider.register('.animate', function() {
+ return {
+ addClass : track('addClass'),
+ removeClass : track('removeClass')
+ };
+ });
+ });
+ inject(function($rootScope, $animate, $compile, $rootElement, $document) {
+ $animate.enabled(true);
+
+ var element = $compile('')($rootScope);
+ $rootElement.append(element);
+ angular.element($document[0].body).append($rootElement);
+
+ $animate.addClass(element, 'one');
+ $animate.addClass(element, 'two');
+ $animate.removeClass(element, 'three');
+ $animate.removeClass(element, 'four');
+ $animate.setClass(element, 'four five', 'two');
+
+ $rootScope.$digest();
+ $animate.triggerReflow();
+
+ expect(log.length).toBe(2);
+ expect(log[0]).toEqual({ name : 'addClass', className : 'one five' });
+ expect(log[1]).toEqual({ name : 'removeClass', className : 'three' });
+ });
+ });
+
+ it('should call class-based animation callbacks in the correct order when animations are skipped', function() {
+ var continueAnimation;
+ module(function($animateProvider) {
+ $animateProvider.register('.animate', function() {
+ return {
+ addClass : function(element, className, done) {
+ continueAnimation = done;
+ }
+ };
+ });
+ });
+ inject(function($rootScope, $animate, $compile, $rootElement, $document) {
+ $animate.enabled(true);
+
+ var element = $compile('')($rootScope);
+ $rootElement.append(element);
+ angular.element($document[0].body).append($rootElement);
+
+ var log = '';
+ $animate.addClass(element, 'one').then(function() {
+ log += 'A';
+ });
+ $rootScope.$digest();
+
+ $animate.addClass(element, 'one').then(function() {
+ log += 'B';
+ });
+ $rootScope.$digest();
+ $animate.triggerCallbackPromise();
+
+ $animate.triggerReflow();
+ continueAnimation();
+ $animate.triggerCallbackPromise();
+ expect(log).toBe('BA');
+ });
+ });
+
+ it('should skip class-based animations when add class and remove class cancel each other out', function() {
+ var spy = jasmine.createSpy();
+ module(function($animateProvider) {
+ $animateProvider.register('.animate', function() {
+ return {
+ addClass : spy,
+ removeClass : spy,
+ };
+ });
+ });
+ inject(function($rootScope, $animate, $compile) {
+ $animate.enabled(true);
+
+ var element = $compile('')($rootScope);
+
+ var count = 0;
+ var callback = function() {
+ count++;
+ };
+
+ $animate.addClass(element, 'on').then(callback);
+ $animate.addClass(element, 'on').then(callback);
+ $animate.removeClass(element, 'on').then(callback);
+ $animate.removeClass(element, 'on').then(callback);
+
+ $rootScope.$digest();
+ $animate.triggerCallbackPromise();
+
+ expect(spy).not.toHaveBeenCalled();
+ expect(count).toBe(4);
+ });
+ });
it("should wait until a queue of animations are complete before performing a reflow",
inject(function($rootScope, $compile, $timeout, $sniffer, $animate) {
@@ -3217,6 +3504,7 @@ describe("ngAnimate", function() {
$animate.enabled(true, element);
$animate.addClass(child, 'awesome');
+ $rootScope.$digest();
$animate.triggerReflow();
expect(childAnimated).toBe(true);
@@ -3224,6 +3512,7 @@ describe("ngAnimate", function() {
$animate.enabled(false, element);
$animate.addClass(child, 'super');
+ $rootScope.$digest();
$animate.triggerReflow();
expect(childAnimated).toBe(false);
@@ -3283,6 +3572,7 @@ describe("ngAnimate", function() {
continueAnimation();
$animate.addClass(child1, 'test');
+ $rootScope.$digest();
$animate.triggerReflow();
expect(child1.hasClass('test')).toBe(true);
@@ -3305,6 +3595,7 @@ describe("ngAnimate", function() {
$animate.triggerCallbacks();
$animate.addClass(child2, 'testing');
+ $rootScope.$digest();
expect(intercepted).toBe('move');
continueAnimation();
@@ -3445,9 +3736,11 @@ describe("ngAnimate", function() {
jqLite($document[0].body).append($rootElement);
$animate.addClass(element, 'green');
+ $rootScope.$digest();
expect(element.hasClass('green-add')).toBe(true);
$animate.addClass(element, 'red');
+ $rootScope.$digest();
expect(element.hasClass('red-add')).toBe(true);
expect(element.hasClass('green')).toBe(false);
@@ -3529,13 +3822,17 @@ describe("ngAnimate", function() {
jqLite($document[0].body).append($rootElement);
$animate.addClass(element, 'on');
+ $rootScope.$digest();
expect(currentAnimation).toBe('addClass');
currentFn();
currentAnimation = null;
$animate.removeClass(element, 'on');
+ $rootScope.$digest();
+
$animate.addClass(element, 'on');
+ $rootScope.$digest();
expect(currentAnimation).toBe('addClass');
});
@@ -3557,11 +3854,13 @@ describe("ngAnimate", function() {
$rootElement.addClass('animated');
$animate.addClass($rootElement, 'green');
+ $rootScope.$digest();
$animate.triggerReflow();
expect(count).toBe(1);
$animate.addClass($rootElement, 'red');
+ $rootScope.$digest();
$animate.triggerReflow();
expect(count).toBe(2);
@@ -3592,6 +3891,8 @@ describe("ngAnimate", function() {
$rootElement.append(element);
$animate.addClass(element, 'red');
+ $rootScope.$digest();
+
$animate.triggerReflow();
expect(steps).toEqual(['before','after']);
@@ -3648,12 +3949,14 @@ describe("ngAnimate", function() {
jqLite($document[0].body).append($rootElement);
$animate.removeClass(element, 'base-class one two');
+ $rootScope.$digest();
//still true since we're before the reflow
expect(element.hasClass('base-class')).toBe(true);
//this will cancel the remove animation
$animate.addClass(element, 'base-class one two');
+ $rootScope.$digest();
//the cancellation was a success and the class was removed right away
expect(element.hasClass('base-class')).toBe(false);
@@ -3694,6 +3997,7 @@ describe("ngAnimate", function() {
expect(capturedProperty).toBe('none');
$animate.addClass(element, 'trigger-class');
+ $rootScope.$digest();
$animate.triggerReflow();
@@ -3719,6 +4023,7 @@ describe("ngAnimate", function() {
var animationKey = $sniffer.vendorPrefix == 'Webkit' ? 'WebkitAnimation' : 'animation';
$animate.addClass(element, 'trigger-class');
+ $rootScope.$digest();
expect(node.style[animationKey]).not.toContain('none');
@@ -3765,6 +4070,7 @@ describe("ngAnimate", function() {
jqLite($document[0].body).append($rootElement);
$animate.addClass(element, 'some-klass');
+ $rootScope.$digest();
var prop = $sniffer.vendorPrefix == 'Webkit' ? 'WebkitAnimation' : 'animation';
@@ -3842,7 +4148,7 @@ describe("ngAnimate", function() {
forEach(element.children(), function(kid) {
browserTrigger(kid, 'transitionend', { timeStamp: Date.now() + 1000, elapsedTime: 1 });
});
- $animate.triggerCallbacks();
+ $animate.triggerCallbackPromise();
$rootScope.items = [];
$rootScope.$digest();
@@ -3878,12 +4184,12 @@ describe("ngAnimate", function() {
jqLite($document[0].body).append($rootElement);
var enterDone = false;
- $animate.enter(element, $rootElement, null, function() {
+ $animate.enter(element, $rootElement).then(function() {
enterDone = true;
});
$rootScope.$digest();
- $animate.triggerCallbacks();
+ $animate.triggerCallbackPromise();
expect(captures['enter']).toBeUndefined();
expect(enterDone).toBe(true);
@@ -3891,12 +4197,12 @@ describe("ngAnimate", function() {
element.addClass('prefixed-animation');
var leaveDone = false;
- $animate.leave(element, function() {
+ $animate.leave(element).then(function() {
leaveDone = true;
});
$rootScope.$digest();
- $animate.triggerCallbacks();
+ $animate.triggerCallbackPromise();
expect(captures['leave']).toBe(true);
expect(leaveDone).toBe(true);
@@ -3934,14 +4240,14 @@ describe("ngAnimate", function() {
var element = upperElement.find('span');
var leaveDone = false;
- $animate.leave(element, function() {
+ $animate.leave(element).then(function() {
leaveDone = true;
});
$rootScope.$digest();
$animate.triggerCallbacks();
- expect(captures['leave']).toBe(true);
+ expect(captures.leave).toBe(true);
expect(leaveDone).toBe(true);
});
});
@@ -3964,9 +4270,10 @@ describe("ngAnimate", function() {
jqLite($document[0].body).append($rootElement);
var ready = false;
- $animate.addClass(element, 'on', function() {
+ $animate.addClass(element, 'on').then(function() {
ready = true;
});
+ $rootScope.$digest();
$animate.triggerReflow();
browserTrigger(element, 'transitionend', { timeStamp: Date.now(), elapsedTime: 1 });
@@ -3974,17 +4281,18 @@ describe("ngAnimate", function() {
browserTrigger(element, 'transitionend', { timeStamp: Date.now(), elapsedTime: 5 });
$animate.triggerReflow();
- $animate.triggerCallbacks();
+ $animate.triggerCallbackPromise();
expect(ready).toBe(true);
ready = false;
- $animate.removeClass(element, 'on', function() {
+ $animate.removeClass(element, 'on').then(function() {
ready = true;
});
+ $rootScope.$digest();
$animate.triggerReflow();
browserTrigger(element, 'transitionend', { timeStamp: Date.now(), elapsedTime: 1 });
- $animate.triggerCallbacks();
+ $animate.triggerCallbackPromise();
expect(ready).toBe(true);
}));
@@ -4003,12 +4311,13 @@ describe("ngAnimate", function() {
jqLite($document[0].body).append($rootElement);
var ready = false;
- $animate.removeClass(element, 'on', function() {
+ $animate.removeClass(element, 'on').then(function() {
ready = true;
});
+ $rootScope.$digest();
$animate.triggerReflow();
- $animate.triggerCallbacks();
+ $animate.triggerCallbackPromise();
expect(ready).toBe(true);
}));
@@ -4027,19 +4336,22 @@ describe("ngAnimate", function() {
jqLite($document[0].body).append($rootElement);
var signature = '';
- $animate.removeClass(element, 'on', function() {
+ $animate.removeClass(element, 'on').then(function() {
signature += 'A';
});
- $animate.addClass(element, 'on', function() {
+ $rootScope.$digest();
+
+ $animate.addClass(element, 'on').then(function() {
signature += 'B';
});
+ $rootScope.$digest();
$animate.triggerReflow();
- $animate.triggerCallbacks();
+ $animate.triggerCallbackPromise();
expect(signature).toBe('A');
browserTrigger(element, 'transitionend', { timeStamp: Date.now(), elapsedTime: 2000 });
- $animate.triggerCallbacks();
+ $animate.triggerCallbackPromise();
expect(signature).toBe('AB');
}));
@@ -4066,7 +4378,10 @@ describe("ngAnimate", function() {
expect(cancelReflowCallback).not.toHaveBeenCalled();
$animate.addClass(element, 'fast');
+ $rootScope.$digest();
+
$animate.addClass(element, 'smooth');
+ $rootScope.$digest();
$animate.triggerReflow();
expect(cancelReflowCallback).toHaveBeenCalled();
diff --git a/test/ngRoute/directive/ngViewSpec.js b/test/ngRoute/directive/ngViewSpec.js
index 2aa286e85f5e..a832a7b32cc8 100644
--- a/test/ngRoute/directive/ngViewSpec.js
+++ b/test/ngRoute/directive/ngViewSpec.js
@@ -773,7 +773,10 @@ describe('ngView animations', function() {
$rootScope.klass = 'boring';
$rootScope.$digest();
- expect($animate.queue.shift().event).toBe('setClass');
+ expect($animate.queue.shift().event).toBe('addClass');
+ expect($animate.queue.shift().event).toBe('removeClass');
+
+ $animate.triggerReflow();
expect(item.hasClass('classy')).toBe(false);
expect(item.hasClass('boring')).toBe(true);
@@ -843,14 +846,12 @@ describe('ngView animations', function() {
it('should destroy the previous leave animation if a new one takes place', function() {
module(function($provide) {
- $provide.value('$animate', {
- enabled : function() { return true; },
- leave : function() {
- //DOM operation left blank
- },
- enter : function(element, parent, after) {
- angular.element(after).after(element);
- }
+ $provide.decorator('$animate', function($delegate, $$q) {
+ var emptyPromise = $$q.defer().promise;
+ $delegate.leave = function() {
+ return emptyPromise;
+ };
+ return $delegate;
});
});
inject(function ($compile, $rootScope, $animate, $location) {