diff --git a/src/ngAnimate/animate.js b/src/ngAnimate/animate.js index 696e80b16dd6..ca136a504942 100644 --- a/src/ngAnimate/animate.js +++ b/src/ngAnimate/animate.js @@ -201,9 +201,9 @@ angular.module('ngAnimate', ['ng']) var NG_ANIMATE_STATE = '$$ngAnimateState'; var rootAnimateState = {running:true}; - $provide.decorator('$animate', ['$delegate', '$injector', '$sniffer', '$rootElement', '$timeout', - function($delegate, $injector, $sniffer, $rootElement, $timeout) { - + $provide.decorator('$animate', ['$delegate', '$injector', '$sniffer', '$rootElement', '$timeout', '$rootScope', + function($delegate, $injector, $sniffer, $rootElement, $timeout, $rootScope) { + $rootElement.data(NG_ANIMATE_STATE, rootAnimateState); function lookup(name) { @@ -282,8 +282,10 @@ angular.module('ngAnimate', ['ng']) */ enter : function(element, parent, after, done) { $delegate.enter(element, parent, after); - performAnimation('enter', 'ng-enter', element, parent, after, function() { - done && $timeout(done, 0, false); + $rootScope.$$postDigest(function() { + performAnimation('enter', 'ng-enter', element, parent, after, function() { + done && $timeout(done, 0, false); + }); }); }, @@ -315,8 +317,10 @@ angular.module('ngAnimate', ['ng']) * @param {function()=} done callback function that will be called once the animation is complete */ leave : function(element, done) { - performAnimation('leave', 'ng-leave', element, null, null, function() { - $delegate.leave(element, done); + $rootScope.$$postDigest(function() { + performAnimation('leave', 'ng-leave', element, null, null, function() { + $delegate.leave(element, done); + }); }); }, @@ -352,8 +356,10 @@ angular.module('ngAnimate', ['ng']) */ move : function(element, parent, after, done) { $delegate.move(element, parent, after); - performAnimation('move', 'ng-move', element, null, null, function() { - done && $timeout(done, 0, false); + $rootScope.$$postDigest(function() { + performAnimation('move', 'ng-move', element, null, null, function() { + done && $timeout(done, 0, false); + }); }); }, @@ -550,6 +556,7 @@ angular.module('ngAnimate', ['ng']) var durationKey = 'Duration', delayKey = 'Delay', + propertyKey = 'Property', animationIterationCountKey = 'IterationCount', ELEMENT_NODE = 1; @@ -610,6 +617,13 @@ angular.module('ngAnimate', ['ng']) timeout is empty (this would cause a flicker bug normally in the page */ if(duration > 0) { + var node = element[0]; + + //temporarily disable the transition so that the enter styles + //don't animate twice (this is here to avoid a bug in Chrome/FF). + node.style[w3cTransitionProp + propertyKey] = 'none'; + node.style[vendorTransitionProp + propertyKey] = 'none'; + var activeClassName = ''; forEach(className.split(' '), function(klass, i) { activeClassName += (i > 0 ? ' ' : '') + klass + '-active'; @@ -617,6 +631,8 @@ angular.module('ngAnimate', ['ng']) //this triggers a reflow which allows for the transition animation to kick in element.prop('clientWidth'); + node.style[w3cTransitionProp + propertyKey] = ''; + node.style[vendorTransitionProp + propertyKey] = ''; element.addClass(activeClassName); $timeout(done, duration * 1000, false); diff --git a/test/ng/directive/ngClassSpec.js b/test/ng/directive/ngClassSpec.js index a7148b6c88a5..f0989f4a9531 100644 --- a/test/ng/directive/ngClassSpec.js +++ b/test/ng/directive/ngClassSpec.js @@ -308,40 +308,107 @@ describe('ngClass', function() { describe('ngClass animations', function() { var body, element, $rootElement; - beforeEach(module('mock.animate')); - - it("should avoid calling addClass accidentally when removeClass is going on", + it("should avoid calling addClass accidentally when removeClass is going on", function() { + module('mock.animate'); inject(function($compile, $rootScope, $animate, $timeout) { + var element = angular.element('
'); + var body = jqLite(document.body); + body.append(element); + $compile(element)($rootScope); - var element = angular.element('
'); - var body = jqLite(document.body); - body.append(element); - $compile(element)($rootScope); + expect($animate.queue.length).toBe(0); - expect($animate.queue.length).toBe(0); + $rootScope.val = 'one'; + $rootScope.$digest(); + $animate.flushNext('addClass'); + $animate.flushNext('addClass'); + expect($animate.queue.length).toBe(0); - $rootScope.val = 'one'; - $timeout.flush(); + $rootScope.val = ''; + $rootScope.$digest(); + $animate.flushNext('removeClass'); //only removeClass is called + expect($animate.queue.length).toBe(0); - $rootScope.$digest(); - $animate.flushNext('addClass'); - $animate.flushNext('addClass'); - expect($animate.queue.length).toBe(0); + $rootScope.val = 'one'; + $rootScope.$digest(); + $animate.flushNext('addClass'); + expect($animate.queue.length).toBe(0); - $rootScope.val = ''; - $rootScope.$digest(); - $animate.flushNext('removeClass'); //only removeClass is called - expect($animate.queue.length).toBe(0); + $rootScope.val = 'two'; + $rootScope.$digest(); + $animate.flushNext('removeClass'); + $animate.flushNext('addClass'); + expect($animate.queue.length).toBe(0); + }); + }); - $rootScope.val = 'one'; - $rootScope.$digest(); - $animate.flushNext('addClass'); - expect($animate.queue.length).toBe(0); + it("should consider the ngClass expression evaluation before performing an animation", function() { + + //mocks are not used since the enter delegation method is called before addClass and + //it makes it impossible to test to see that addClass is called first + module('ngAnimate'); + + var digestQueue = []; + module(function($animateProvider) { + $animateProvider.register('.crazy', function() { + return { + enter : function(element, done) { + element.data('state', 'crazy-enter'); + done(); + } + }; + }); + + return function($rootScope) { + var before = $rootScope.$$postDigest; + $rootScope.$$postDigest = function() { + var args = arguments; + digestQueue.push(function() { + before.apply($rootScope, args); + }); + }; + }; + }); + inject(function($compile, $rootScope, $rootElement, $animate, $timeout, $document) { - $rootScope.val = 'two'; - $rootScope.$digest(); - $animate.flushNext('removeClass'); - $animate.flushNext('addClass'); - expect($animate.queue.length).toBe(0); - })); + //since we skip animations upon first digest, this needs to be set to true + $animate.enabled(true); + + $rootScope.val = 'crazy'; + var element = angular.element('
'); + jqLite($document[0].body).append($rootElement); + + $compile(element)($rootScope); + + var enterComplete = false; + $animate.enter(element, $rootElement, null, function() { + enterComplete = true; + }); + + //jquery doesn't compare both elements properly so let's use the nodes + expect(element.parent()[0]).toEqual($rootElement[0]); + expect(element.hasClass('crazy')).toBe(false); + expect(enterComplete).toBe(false); + + expect(digestQueue.length).toBe(1); + $rootScope.$digest(); + + $timeout.flush(); + + expect(element.hasClass('crazy')).toBe(true); + expect(enterComplete).toBe(false); + + digestQueue.shift()(); //enter + expect(digestQueue.length).toBe(0); + + //we don't normally need this, but since the timing between digests + //is spaced-out then it is required so that the original digestion + //is kicked into gear + $rootScope.$digest(); + $timeout.flush(); + + expect(element.data('state')).toBe('crazy-enter'); + expect(enterComplete).toBe(true); + }); + }); }); diff --git a/test/ngAnimate/animateSpec.js b/test/ngAnimate/animateSpec.js index 3aab3fecebb8..661cc91e87a6 100644 --- a/test/ngAnimate/animateSpec.js +++ b/test/ngAnimate/animateSpec.js @@ -133,6 +133,7 @@ describe("ngAnimate", function() { expect(element.contents().length).toBe(0); $animate.enter(child, element); + $rootScope.$digest(); if($sniffer.transitions) { expect(child.hasClass('ng-enter')).toBe(true); @@ -148,6 +149,8 @@ describe("ngAnimate", function() { expect(element.contents().length).toBe(1); $animate.leave(child); + $rootScope.$digest(); + if($sniffer.transitions) { expect(child.hasClass('ng-leave')).toBe(true); expect(child.hasClass('ng-leave-active')).toBe(true); @@ -169,6 +172,7 @@ describe("ngAnimate", function() { element.append(child2); expect(element.text()).toBe('12'); $animate.move(child1, element, child2); + $rootScope.$digest(); expect(element.text()).toBe('21'); })); @@ -213,6 +217,8 @@ describe("ngAnimate", function() { //enter $animate.enter(child, element); + $rootScope.$digest(); + expect(child.attr('class')).toContain('ng-enter'); expect(child.attr('class')).toContain('ng-enter-active'); $timeout.flushNext(1000); @@ -220,6 +226,8 @@ describe("ngAnimate", function() { //move element.append(after); $animate.move(child, element, after); + $rootScope.$digest(); + expect(child.attr('class')).toContain('ng-move'); expect(child.attr('class')).toContain('ng-move-active'); $timeout.flushNext(1000); @@ -238,6 +246,7 @@ describe("ngAnimate", function() { //leave $animate.leave(child); + $rootScope.$digest(); expect(child.attr('class')).toContain('ng-leave'); expect(child.attr('class')).toContain('ng-leave-active'); $timeout.flushNext(1000); @@ -274,6 +283,7 @@ describe("ngAnimate", function() { expect(child).toBeShown(); $animate.leave(child); + $rootScope.$digest(); expect(child).toBeHidden(); //hides instantly //lets change this to prove that done doesn't fire anymore for the previous hide() operation @@ -682,6 +692,7 @@ describe("ngAnimate", function() { element[0].className = 'abc'; $animate.enter(element, parent); + $rootScope.$digest(); if ($sniffer.transitions) { expect(element.hasClass('abc ng-enter')).toBe(true); @@ -692,6 +703,7 @@ describe("ngAnimate", function() { element[0].className = 'xyz'; $animate.enter(element, parent); + $rootScope.$digest(); if ($sniffer.transitions) { expect(element.hasClass('xyz')).toBe(true); @@ -717,6 +729,7 @@ describe("ngAnimate", function() { element.attr('class','one two'); $animate.enter(element, parent); + $rootScope.$digest(); if($sniffer.transitions) { expect(element.hasClass('one two ng-enter')).toBe(true); expect(element.hasClass('one two ng-enter ng-enter-active')).toBe(true); @@ -766,6 +779,7 @@ describe("ngAnimate", function() { $animate.enter(element, parent, null, function() { flag = true; }); + $rootScope.$digest(); $timeout.flush(); @@ -784,6 +798,7 @@ describe("ngAnimate", function() { $animate.leave(element, function() { flag = true; }); + $rootScope.$digest(); $timeout.flush(); @@ -803,6 +818,7 @@ describe("ngAnimate", function() { $animate.move(element, parent, parent2, function() { flag = true; }); + $rootScope.$digest(); $timeout.flush(); @@ -1212,6 +1228,7 @@ describe("ngAnimate", function() { var child = $compile('
...
')($rootScope); $animate.enter(child, element); + $rootScope.$digest(); if($sniffer.transitions) { expect(child.hasClass('ng-enter')).toBe(true); @@ -1233,6 +1250,7 @@ describe("ngAnimate", function() { var child = $compile('
...
')($rootScope); $animate.enter(child, element); + $rootScope.$digest(); if($sniffer.transitions) { expect(child.hasClass('ng-enter')).toBe(true); @@ -1257,6 +1275,7 @@ describe("ngAnimate", function() { expect(child.hasClass('ng-enter')).toBe(false); $animate.enter(child, element); + $rootScope.$digest(); expect(child.hasClass('ng-enter')).toBe(false); })); @@ -1283,6 +1302,7 @@ describe("ngAnimate", function() { child.addClass('custom'); $animate.enter(child, element); + $rootScope.$digest(); $timeout.flushNext(10); @@ -1315,6 +1335,7 @@ describe("ngAnimate", function() { var child = $compile('
...
')($rootScope); $animate.enter(child, element); + $rootScope.$digest(); //this is added/removed right away otherwise if($sniffer.transitions) { @@ -1325,6 +1346,7 @@ describe("ngAnimate", function() { expect(child.hasClass('this-is-mine-now')).toBe(false); child.addClass('usurper'); $animate.leave(child); + $rootScope.$digest(); expect(child.hasClass('ng-enter')).toBe(false); expect(child.hasClass('ng-enter-active')).toBe(false); diff --git a/test/ngRoute/directive/ngViewSpec.js b/test/ngRoute/directive/ngViewSpec.js index 818645efb505..1bce2b6280fb 100644 --- a/test/ngRoute/directive/ngViewSpec.js +++ b/test/ngRoute/directive/ngViewSpec.js @@ -637,6 +637,9 @@ describe('ngView animations', function() { $rootScope.$digest(); $animate.flushNext('leave'); //ngView old + + $rootScope.$digest(); + $animate.flushNext('enter'); //ngView new expect(n(element.text())).toEqual(''); //this is midway during the animation @@ -644,6 +647,8 @@ describe('ngView animations', function() { $animate.flushNext('enter'); //ngRepeat 3 $animate.flushNext('enter'); //ngRepeat 4 + $rootScope.$digest(); + expect(element.text()).toEqual('34'); function n(text) {