aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMatias Niemelä2013-08-28 19:32:20 -0400
committerMisko Hevery2013-09-03 17:06:49 -0700
commit36ad40b18cfdd0690411a5169aa94e222946b5cf (patch)
tree221c73bebdd87e91fa56fb3bc55cbf67e59cc0c0
parent4e15c4fb47e93c1f6619a09125bc9a350e39b113 (diff)
downloadangular.js-36ad40b18cfdd0690411a5169aa94e222946b5cf.tar.bz2
fix(ngAnimate): ensure that ngClass is always compiled before enter, leave and move animations
Closes #3727 Closes #3603
-rw-r--r--src/ngAnimate/animate.js34
-rw-r--r--test/ng/directive/ngClassSpec.js123
-rw-r--r--test/ngAnimate/animateSpec.js22
-rw-r--r--test/ngRoute/directive/ngViewSpec.js5
4 files changed, 147 insertions, 37 deletions
diff --git a/src/ngAnimate/animate.js b/src/ngAnimate/animate.js
index 696e80b1..ca136a50 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 a7148b6c..f0989f4a 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('<div ng-class="val"></div>');
+ var body = jqLite(document.body);
+ body.append(element);
+ $compile(element)($rootScope);
- var element = angular.element('<div ng-class="val"></div>');
- 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('<div ng-class="val"></div>');
+ 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 3aab3fec..661cc91e 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('<div>...</div>')($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('<div>...</div>')($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('<div>...</div>')($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 818645ef..1bce2b62 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) {