diff --git a/src/ng/animator.js b/src/ng/animator.js
index e1c3ab48a55b..5080069c57ed 100644
--- a/src/ng/animator.js
+++ b/src/ng/animator.js
@@ -40,6 +40,10 @@
*
* The `event1` and `event2` attributes refer to the animation events specific to the directive that has been assigned.
*
+ * Keep in mind that, by default, **all** initial animations will be skipped until the first digest cycle has fully
+ * passed. This helps prevent any unexpected animations from occurring while the application or directive is initializing. To
+ * override this behavior, you may pass "animateFirst: true" into the ngAnimate attribute expression.
+ *
*
CSS-defined Animations
* By default, ngAnimate attaches two CSS3 classes per animation event to the DOM element to achieve the animation.
* This is up to you, the developer, to ensure that the animations take place using cross-browser CSS3 transitions.
@@ -140,6 +144,30 @@ var $AnimatorProvider = function() {
*/
var AnimatorService = function(scope, attrs) {
var ngAnimateAttr = attrs.ngAnimate;
+
+ // avoid running animations on start
+ var animationEnabled = false;
+ var ngAnimateValue = ngAnimateAttr && scope.$eval(ngAnimateAttr);
+
+ if (!animationEnabled) {
+ if(isObject(ngAnimateValue) && ngAnimateValue['animateFirst']) {
+ animationEnabled = true;
+ } else {
+ var enableSubsequent = function() {
+ removeWatch();
+ scope.$evalAsync(function() {
+ animationEnabled = true;
+ });
+ };
+ var removeWatch = noop;
+
+ if (scope.$$phase) {
+ enableSubsequent();
+ } else {
+ removeWatch = scope.$watch(enableSubsequent);
+ }
+ }
+ }
var animator = {};
/**
@@ -214,7 +242,6 @@ var $AnimatorProvider = function() {
return animator;
function animateActionFactory(type, beforeFn, afterFn) {
- var ngAnimateValue = ngAnimateAttr && scope.$eval(ngAnimateAttr);
var className = ngAnimateAttr
? isObject(ngAnimateValue) ? ngAnimateValue[type] : ngAnimateValue + '-' + type
: '';
@@ -233,7 +260,8 @@ var $AnimatorProvider = function() {
var startClass = className + '-start';
return function(element, parent, after) {
- if (!globalAnimationEnabled || !$sniffer.supportsTransitions && !polyfillSetup && !polyfillStart) {
+ if (!animationEnabled || !globalAnimationEnabled ||
+ (!$sniffer.supportsTransitions && !polyfillSetup && !polyfillStart)) {
beforeFn(element, parent, after);
afterFn(element, parent, after);
return;
@@ -268,7 +296,6 @@ var $AnimatorProvider = function() {
0,
duration);
});
-
$window.setTimeout(done, duration * 1000);
} else {
done();
diff --git a/src/ngMock/angular-mocks.js b/src/ngMock/angular-mocks.js
index 94f099a6c155..14a1f09ecf09 100644
--- a/src/ngMock/angular-mocks.js
+++ b/src/ngMock/angular-mocks.js
@@ -628,7 +628,9 @@ angular.mock.createMockWindow = function() {
if (setTimeoutQueue.length > 0) {
return {
process: function() {
- setTimeoutQueue.shift().fn();
+ var tick = setTimeoutQueue.shift();
+ expect(tick.delay).toEqual(delay);
+ tick.fn();
}
};
} else {
diff --git a/test/ng/animatorSpec.js b/test/ng/animatorSpec.js
index 794574889e8c..7cf785e0bc52 100644
--- a/test/ng/animatorSpec.js
+++ b/test/ng/animatorSpec.js
@@ -2,10 +2,21 @@
describe("$animator", function() {
- var element;
+ var body, element;
+
+ function html(html) {
+ body.html(html);
+ element = body.children().eq(0);
+ return element;
+ }
+
+ beforeEach(function() {
+ // we need to run animation on attached elements;
+ body = jqLite(document.body);
+ });
afterEach(function(){
- dealoc(element);
+ dealoc(body);
});
describe("enable / disable", function() {
@@ -120,6 +131,7 @@ describe("$animator", function() {
animator = $animator($rootScope, {
ngAnimate : '{enter: \'custom\'}'
});
+ $rootScope.$digest(); // re-enable the animations;
expect(element.contents().length).toBe(0);
animator.enter(child, element);
window.setTimeout.expect(1).process();
@@ -129,6 +141,7 @@ describe("$animator", function() {
animator = $animator($rootScope, {
ngAnimate : '{leave: \'custom\'}'
});
+ $rootScope.$digest();
element.append(child);
expect(element.contents().length).toBe(1);
animator.leave(child, element);
@@ -140,6 +153,7 @@ describe("$animator", function() {
animator = $animator($rootScope, {
ngAnimate : '{move: \'custom\'}'
});
+ $rootScope.$digest();
var child1 = $compile('1
')($rootScope);
var child2 = $compile('2
')($rootScope);
element.append(child1);
@@ -154,6 +168,7 @@ describe("$animator", function() {
animator = $animator($rootScope, {
ngAnimate : '{show: \'custom\'}'
});
+ $rootScope.$digest();
element.css('display','none');
expect(element.css('display')).toBe('none');
animator.show(element);
@@ -166,6 +181,7 @@ describe("$animator", function() {
animator = $animator($rootScope, {
ngAnimate : '{hide: \'custom\'}'
});
+ $rootScope.$digest();
element.css('display','block');
expect(element.css('display')).toBe('block');
animator.hide(element);
@@ -181,6 +197,8 @@ describe("$animator", function() {
ngAnimate : '"custom"'
});
+ $rootScope.$digest();
+
//enter
animator.enter(child, element);
expect(child.attr('class')).toContain('custom-enter-setup');
@@ -222,6 +240,7 @@ describe("$animator", function() {
animator = $animator($rootScope, {
ngAnimate : '{show: \'setup-memo\'}'
});
+ $rootScope.$digest();
expect(element.text()).toEqual('');
animator.show(element);
window.setTimeout.expect(1).process();
@@ -234,6 +253,8 @@ describe("$animator", function() {
animator = $animator($rootScope, {
ngAnimate : '{show: \'setup-memo\'}'
});
+ $rootScope.$digest();
+
element.text('123');
expect(element.text()).toBe('123');
animator.show(element);
@@ -262,11 +283,13 @@ describe("$animator", function() {
it("should skip animations if disabled and run when enabled",
inject(function($animator, $rootScope, $compile, $sniffer) {
$animator.enabled(false);
- element = $compile('1
')($rootScope);
+ element = $compile(html('1
'))($rootScope);
var animator = $animator($rootScope, {
ngAnimate : '{show: \'inline-show\'}'
});
+ $rootScope.$digest(); // skip no-animate on first digest.
+
element.css('display','none');
expect(element.css('display')).toBe('none');
animator.show(element);
@@ -289,6 +312,7 @@ describe("$animator", function() {
it("should throw an error when an invalid ng-animate syntax is provided", inject(function($compile, $rootScope) {
expect(function() {
element = $compile('')($rootScope);
+ $rootScope.$digest();
}).toThrow("Syntax Error: Token ':' not a primary expression at column 1 of the expression [:] starting at [:].");
}));
});
diff --git a/test/ng/directive/ngIncludeSpec.js b/test/ng/directive/ngIncludeSpec.js
index 191eaa05e751..4798baccf55e 100644
--- a/test/ng/directive/ngIncludeSpec.js
+++ b/test/ng/directive/ngIncludeSpec.js
@@ -282,7 +282,24 @@ describe('ngInclude', function() {
});
describe('ngInclude ngAnimate', function() {
- var element, vendorPrefix, window;
+ var vendorPrefix, window;
+ var body, element;
+
+ function html(html) {
+ body.html(html);
+ element = body.children().eq(0);
+ return element;
+ }
+
+ beforeEach(function() {
+ // we need to run animation on attached elements;
+ body = jqLite(document.body);
+ });
+
+ afterEach(function(){
+ dealoc(body);
+ dealoc(element);
+ });
beforeEach(module(function($animationProvider, $provide) {
$provide.value('$window', window = angular.mock.createMockWindow());
@@ -300,12 +317,12 @@ describe('ngInclude ngAnimate', function() {
$templateCache.put('enter', [200, 'data
', {}]);
$rootScope.tpl = 'enter';
- element = $compile(
+ element = $compile(html(
'' +
'
'
- )($rootScope);
+ ))($rootScope);
$rootScope.$digest();
//if we add the custom css stuff here then it will get picked up before the animation takes place
@@ -332,12 +349,12 @@ describe('ngInclude ngAnimate', function() {
inject(function($compile, $rootScope, $templateCache, $sniffer) {
$templateCache.put('enter', [200, 'data
', {}]);
$rootScope.tpl = 'enter';
- element = $compile(
+ element = $compile(html(
'' +
'
'
- )($rootScope);
+ ))($rootScope);
$rootScope.$digest();
//if we add the custom css stuff here then it will get picked up before the animation takes place
@@ -367,12 +384,12 @@ describe('ngInclude ngAnimate', function() {
inject(function($compile, $rootScope, $templateCache, $sniffer) {
$templateCache.put('enter', [200, 'data
', {}]);
$rootScope.tpl = 'enter';
- element = $compile(
+ element = $compile(html(
'' +
'
'
- )($rootScope);
+ ))($rootScope);
$rootScope.$digest();
//if we add the custom css stuff here then it will get picked up before the animation takes place
diff --git a/test/ng/directive/ngRepeatSpec.js b/test/ng/directive/ngRepeatSpec.js
index 533b83c80da1..070e6e02e27a 100644
--- a/test/ng/directive/ngRepeatSpec.js
+++ b/test/ng/directive/ngRepeatSpec.js
@@ -513,7 +513,24 @@ describe('ngRepeat', function() {
});
describe('ngRepeat ngAnimate', function() {
- var element, vendorPrefix, window;
+ var vendorPrefix, window;
+ var body, element;
+
+ function html(html) {
+ body.html(html);
+ element = body.children().eq(0);
+ return element;
+ }
+
+ beforeEach(function() {
+ // we need to run animation on attached elements;
+ body = jqLite(document.body);
+ });
+
+ afterEach(function(){
+ dealoc(body);
+ dealoc(element);
+ });
beforeEach(module(function($animationProvider, $provide) {
$provide.value('$window', window = angular.mock.createMockWindow());
@@ -522,20 +539,18 @@ describe('ngRepeat ngAnimate', function() {
};
}));
- afterEach(function(){
- dealoc(element);
- });
-
it('should fire off the enter animation + add and remove the css classes',
inject(function($compile, $rootScope, $sniffer) {
- element = $compile(
+ element = $compile(html(
''
- )($rootScope);
+ ))($rootScope);
+
+ $rootScope.$digest(); // re-enable the animations;
$rootScope.items = ['1','2','3'];
$rootScope.$digest();
@@ -572,13 +587,13 @@ describe('ngRepeat ngAnimate', function() {
it('should fire off the leave animation + add and remove the css classes',
inject(function($compile, $rootScope, $sniffer) {
- element = $compile(
+ element = $compile(html(
''
- )($rootScope);
+ ))($rootScope);
$rootScope.items = ['1','2','3'];
$rootScope.$digest();
@@ -612,13 +627,13 @@ describe('ngRepeat ngAnimate', function() {
it('should fire off the move animation + add and remove the css classes',
inject(function($compile, $rootScope, $sniffer) {
- element = $compile(
+ element = $compile(html(
'' +
'
' +
'{{ item }}' +
'
' +
'
'
- )($rootScope);
+ ))($rootScope);
$rootScope.items = ['1','2','3'];
$rootScope.$digest();
@@ -666,13 +681,15 @@ describe('ngRepeat ngAnimate', function() {
it('should catch and use the correct duration for animation',
inject(function($compile, $rootScope, $sniffer) {
- element = $compile(
+ element = $compile(html(
''
- )($rootScope);
+ ))($rootScope);
+
+ $rootScope.$digest(); // re-enable the animations;
$rootScope.items = ['a','b'];
$rootScope.$digest();
diff --git a/test/ng/directive/ngShowHideSpec.js b/test/ng/directive/ngShowHideSpec.js
index d1d314e72912..17c47255b171 100644
--- a/test/ng/directive/ngShowHideSpec.js
+++ b/test/ng/directive/ngShowHideSpec.js
@@ -43,8 +43,25 @@ describe('ngShow / ngHide', function() {
});
describe('ngShow / ngHide - ngAnimate', function() {
- var element, window;
+ var window;
var vendorPrefix;
+ var body, element;
+
+ function html(html) {
+ body.html(html);
+ element = body.children().eq(0);
+ return element;
+ }
+
+ beforeEach(function() {
+ // we need to run animation on attached elements;
+ body = jqLite(document.body);
+ });
+
+ afterEach(function(){
+ dealoc(body);
+ dealoc(element);
+ });
beforeEach(module(function($animationProvider, $provide) {
$provide.value('$window', window = angular.mock.createMockWindow());
@@ -53,21 +70,17 @@ describe('ngShow / ngHide - ngAnimate', function() {
};
}));
- afterEach(function() {
- dealoc(element);
- });
-
describe('ngShow', function() {
it('should fire off the animator.show and animator.hide animation', inject(function($compile, $rootScope, $sniffer) {
var $scope = $rootScope.$new();
$scope.on = true;
- element = $compile(
+ element = $compile(html(
'' +
+ 'ng-animate="{show: \'custom-show\', hide: \'custom-hide\', animateFirst: true}">' +
'
'
- )($scope);
+ ))($scope);
$scope.$digest();
if ($sniffer.supportsTransitions) {
@@ -97,19 +110,43 @@ describe('ngShow / ngHide - ngAnimate', function() {
expect(element.attr('class')).not.toContain('custom-hide-start');
expect(element.attr('class')).not.toContain('custom-hide-setup');
}));
+
+ it('should skip the initial show state on the first digest', function() {
+ var fired = false;
+ inject(function($compile, $rootScope, $sniffer) {
+ $rootScope.val = true;
+ var element = $compile(html('123
'))($rootScope);
+ element.css('display','none');
+ expect(element.css('display')).toBe('none');
+
+ $rootScope.$digest();
+ expect(element[0].style.display).toBe('');
+ expect(fired).toBe(false);
+
+ $rootScope.val = false;
+ $rootScope.$digest();
+ if ($sniffer.supportsTransitions) {
+ window.setTimeout.expect(1).process();
+ window.setTimeout.expect(0).process();
+ } else {
+ expect(window.setTimeout.queue).toEqual([]);
+ }
+ expect(element[0].style.display).toBe('none');
+ });
+ });
});
describe('ngHide', function() {
it('should fire off the animator.show and animator.hide animation', inject(function($compile, $rootScope, $sniffer) {
var $scope = $rootScope.$new();
$scope.off = true;
- element = $compile(
+ element = $compile(html(
'' +
- '
'
- )($scope);
+ 'ng-animate="{show: \'custom-show\', hide: \'custom-hide\', animateFirst: true}">' +
+ ''
+ ))($scope);
$scope.$digest();
if ($sniffer.supportsTransitions) {
@@ -140,5 +177,31 @@ describe('ngShow / ngHide - ngAnimate', function() {
expect(element.attr('class')).not.toContain('custom-show-start');
expect(element.attr('class')).not.toContain('custom-show-setup');
}));
+
+ it('should skip the initial hide state on the first digest', function() {
+ var fired = false;
+ module(function($animationProvider) {
+ $animationProvider.register('destructive-animation', function() {
+ return {
+ setup : function() {},
+ start : function(element, done) {
+ fired = true;
+ }
+ };
+ });
+ });
+ inject(function($compile, $rootScope) {
+ $rootScope.val = false;
+ var element = $compile(html('123
'))($rootScope);
+ element.css('display','block');
+ expect(element.css('display')).toBe('block');
+
+ $rootScope.val = true;
+ $rootScope.$digest();
+
+ expect(element.css('display')).toBe('none');
+ expect(fired).toBe(false);
+ });
+ });
});
});
diff --git a/test/ng/directive/ngSwitchSpec.js b/test/ng/directive/ngSwitchSpec.js
index 9d3eceaa0dd3..5f0a2bb3427c 100644
--- a/test/ng/directive/ngSwitchSpec.js
+++ b/test/ng/directive/ngSwitchSpec.js
@@ -215,7 +215,24 @@ describe('ngSwitch', function() {
});
describe('ngSwitch ngAnimate', function() {
- var element, vendorPrefix, window;
+ var vendorPrefix, window;
+ var body, element;
+
+ function html(html) {
+ body.html(html);
+ element = body.children().eq(0);
+ return element;
+ }
+
+ beforeEach(function() {
+ // we need to run animation on attached elements;
+ body = jqLite(document.body);
+ });
+
+ afterEach(function(){
+ dealoc(body);
+ dealoc(element);
+ });
beforeEach(module(function($animationProvider, $provide) {
$provide.value('$window', window = angular.mock.createMockWindow());
@@ -224,22 +241,19 @@ describe('ngSwitch ngAnimate', function() {
};
}));
- afterEach(function(){
- dealoc(element);
- });
-
it('should fire off the enter animation + set and remove the classes',
inject(function($compile, $rootScope, $sniffer) {
var $scope = $rootScope.$new();
var style = vendorPrefix + 'transition: 1s linear all';
- element = $compile(
+ element = $compile(html(
'' +
'
one
' +
'
two
' +
'
three
' +
'
'
- )($scope);
+ ))($scope);
+ $rootScope.$digest(); // re-enable the animations;
$scope.val = 'one';
$scope.$digest();
@@ -265,14 +279,15 @@ describe('ngSwitch ngAnimate', function() {
inject(function($compile, $rootScope, $sniffer) {
var $scope = $rootScope.$new();
var style = vendorPrefix + 'transition: 1s linear all';
- element = $compile(
+ element = $compile(html(
'' +
'
one
' +
'
two
' +
'
three
' +
'
'
- )($scope);
+ ))($scope);
+ $rootScope.$digest(); // re-enable the animations;
$scope.val = 'two';
$scope.$digest();
@@ -313,12 +328,13 @@ describe('ngSwitch ngAnimate', function() {
it('should catch and use the correct duration for animation',
inject(function($compile, $rootScope, $sniffer) {
- element = $compile(
+ element = $compile(html(
''
- )($rootScope);
+ ))($rootScope);
+ $rootScope.$digest(); // re-enable the animations;
$rootScope.val = 'one';
$rootScope.$digest();
diff --git a/test/ng/directive/ngViewSpec.js b/test/ng/directive/ngViewSpec.js
index dcdfe6862388..c0348568983a 100644
--- a/test/ng/directive/ngViewSpec.js
+++ b/test/ng/directive/ngViewSpec.js
@@ -486,7 +486,26 @@ describe('ngView', function() {
});
describe('ngAnimate', function() {
- var element, window;
+ var window;
+ var body, element;
+
+ function html(html) {
+ body.html(html);
+ element = body.children().eq(0);
+ return element;
+ }
+
+ beforeEach(function() {
+ // we need to run animation on attached elements;
+ body = jqLite(document.body);
+ });
+
+ afterEach(function(){
+ dealoc(body);
+ dealoc(element);
+ });
+
+
beforeEach(module(function($provide, $routeProvider) {
$provide.value('$window', window = angular.mock.createMockWindow());
@@ -496,13 +515,9 @@ describe('ngAnimate', function() {
}
}));
- afterEach(function(){
- dealoc(element);
- });
-
it('should fire off the enter animation + add and remove the css classes',
inject(function($compile, $rootScope, $sniffer, $location, $templateCache) {
- element = $compile('')($rootScope);
+ element = $compile(html(''))($rootScope);
$location.path('/foo');
$rootScope.$digest();
@@ -530,7 +545,7 @@ describe('ngAnimate', function() {
it('should fire off the leave animation + add and remove the css classes',
inject(function($compile, $rootScope, $sniffer, $location, $templateCache) {
$templateCache.put('/foo.html', [200, 'foo
', {}]);
- element = $compile('')($rootScope);
+ element = $compile(html(''))($rootScope);
$location.path('/foo');
$rootScope.$digest();
@@ -561,12 +576,12 @@ describe('ngAnimate', function() {
it('should catch and use the correct duration for animations',
inject(function($compile, $rootScope, $sniffer, $location, $templateCache) {
$templateCache.put('/foo.html', [200, 'foo
', {}]);
- element = $compile(
+ element = $compile(html(
'' +
+ 'ng-animate="{enter: \'customEnter\', animateFirst: false}">' +
'
'
- )($rootScope);
+ ))($rootScope);
$location.path('/foo');
$rootScope.$digest();