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( '
' + '{{ item }}' + '
' - )($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( '
' + '{{ item }}' + '
' - )($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( '
' + '{{ item }}' + '
' - )($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( '
' + '
one
' + '
' - )($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();