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) {