diff --git a/docs/content/guide/animations.ngdoc b/docs/content/guide/animations.ngdoc index 14b62f1f9d1b..0898789dd917 100644 --- a/docs/content/guide/animations.ngdoc +++ b/docs/content/guide/animations.ngdoc @@ -274,6 +274,37 @@ myModule.directive('my-directive', ['$animate', function($animate) { }]); ``` +## Preventing flicker before an animation starts + +When nesting elements with structural animations such as `ngIf` into elements that have class-based +animations such as `ngClass`, it sometimes happens that before the actual animation starts, there is a brief flicker or flash of content +where the animated element is briefly visible. + +To prevent this, you can apply styles to the `ng-[event]-prepare` class, which is added as soon as an animation is initialized, +but removed before the actual animation starts (after waiting for a $digest). This class is only added for *structural* +animations (`enter`, `move`, and `leave`). + +Here's an example where you might see flickering: + +```html +
+
+
+
+
+``` + +It is possible that during the `enter` event, the `.message` div will be briefly visible before it starts animating. +In that case, you can add styles to the CSS that make sure the element stays hidden before the animation starts: + +```css +.message.ng-enter-prepare { + opacity: 0; +} + +/* Other animation styles ... */ +``` + ## More about animations For a full breakdown of each method available on `$animate`, see the {@link ng.$animate API documentation}. diff --git a/src/ngAnimate/.jshintrc b/src/ngAnimate/.jshintrc index 187ecd1f3c9d..9ab8bbe1b890 100644 --- a/src/ngAnimate/.jshintrc +++ b/src/ngAnimate/.jshintrc @@ -29,6 +29,7 @@ "REMOVE_CLASS_SUFFIX": false, "EVENT_CLASS_PREFIX": false, "ACTIVE_CLASS_SUFFIX": false, + "PREPARE_CLASS_SUFFIX": false, "TRANSITION_DURATION_PROP": false, "TRANSITION_DELAY_PROP": false, diff --git a/src/ngAnimate/animation.js b/src/ngAnimate/animation.js index c0deb035f790..f0ac060fcb9d 100644 --- a/src/ngAnimate/animation.js +++ b/src/ngAnimate/animation.js @@ -135,6 +135,12 @@ var $$AnimationProvider = ['$animateProvider', function($animateProvider) { options.tempClasses = null; } + var prepareClassName; + if (isStructural) { + prepareClassName = 'ng-' + event + PREPARE_CLASS_SUFFIX; + $$jqLite.addClass(element, prepareClassName); + } + animationQueue.push({ // this data is used by the postDigest code and passed into // the driver step function @@ -357,6 +363,10 @@ var $$AnimationProvider = ['$animateProvider', function($animateProvider) { if (tempClasses) { $$jqLite.addClass(element, tempClasses); } + if (prepareClassName) { + $$jqLite.removeClass(element, prepareClassName); + prepareClassName = null; + } } function updateAnimationRunners(animation, newRunner) { diff --git a/src/ngAnimate/module.js b/src/ngAnimate/module.js index e6bb8208d56c..c1178902db13 100644 --- a/src/ngAnimate/module.js +++ b/src/ngAnimate/module.js @@ -254,6 +254,34 @@ * the CSS class once an animation has completed.) * * + * ### The `ng-[event]-prepare` class + * + * This is a special class that can be used to prevent unwanted flickering / flash of content before + * the actual animation starts. The class is added as soon as an animation is initialized, but removed + * before the actual animation starts (after waiting for a $digest). + * It is also only added for *structural* animations (`enter`, `move`, and `leave`). + * + * In practice, flickering can appear when nesting elements with structural animations such as `ngIf` + * into elements that have class-based animations such as `ngClass`. + * + * ```html + *
+ *
+ *
+ *
+ *
+ * ``` + * + * It is possible that during the `enter` animation, the `.message` div will be briefly visible before it starts animating. + * In that case, you can add styles to the CSS that make sure the element stays hidden before the animation starts: + * + * ```css + * .message.ng-enter-prepare { + * opacity: 0; + * } + * + * ``` + * * ## JavaScript-based Animations * * ngAnimate also allows for animations to be consumed by JavaScript code. The approach is similar to CSS-based animations (where there is a shared diff --git a/src/ngAnimate/shared.js b/src/ngAnimate/shared.js index 634cd90bada4..501e5bd0d015 100644 --- a/src/ngAnimate/shared.js +++ b/src/ngAnimate/shared.js @@ -21,6 +21,7 @@ var ADD_CLASS_SUFFIX = '-add'; var REMOVE_CLASS_SUFFIX = '-remove'; var EVENT_CLASS_PREFIX = 'ng-'; var ACTIVE_CLASS_SUFFIX = '-active'; +var PREPARE_CLASS_SUFFIX = '-prepare'; var NG_ANIMATE_CLASSNAME = 'ng-animate'; var NG_ANIMATE_CHILDREN_DATA = '$$ngAnimateChildren'; diff --git a/test/ngAnimate/animationSpec.js b/test/ngAnimate/animationSpec.js index 04ba3e5949a3..dcce9c1219f7 100644 --- a/test/ngAnimate/animationSpec.js +++ b/test/ngAnimate/animationSpec.js @@ -513,6 +513,25 @@ describe('$$animation', function() { expect(captureLog[1].element).toBe(child); expect(captureLog[2].element).toBe(grandchild); })); + + + they('should add the preparation class before the $prop-animation is pushed to the queue', + ['enter', 'leave', 'move'], function(animationType) { + inject(function($$animation, $rootScope, $animate) { + var runner = $$animation(element, animationType); + expect(element).toHaveClass('ng-' + animationType + '-prepare'); + }); + }); + + + they('should remove the preparation class before the $prop-animation starts', + ['enter', 'leave', 'move'], function(animationType) { + inject(function($$animation, $rootScope, $$rAF) { + var runner = $$animation(element, animationType); + $rootScope.$digest(); + expect(element).not.toHaveClass('ng-' + animationType + '-prepare'); + }); + }); }); describe("grouped", function() { diff --git a/test/ngAnimate/integrationSpec.js b/test/ngAnimate/integrationSpec.js index feb28e581456..4a0610f2170a 100644 --- a/test/ngAnimate/integrationSpec.js +++ b/test/ngAnimate/integrationSpec.js @@ -268,6 +268,42 @@ describe('ngAnimate integration tests', function() { }); }); + it('should add the preparation class for an enter animation before a parent class-based animation is applied', function() { + module('ngAnimateMock'); + inject(function($animate, $compile, $rootScope, $rootElement, $document) { + element = jqLite( + '
' + + '
' + + '
' + + '
' + ); + + ss.addRule('.ng-enter', 'transition:2s linear all;'); + ss.addRule('.parent-add', 'transition:5s linear all;'); + + $rootElement.append(element); + jqLite($document[0].body).append($rootElement); + + $compile(element)($rootScope); + $rootScope.exp = true; + $rootScope.$digest(); + + var parent = element; + var child = element.find('div'); + + expect(parent).not.toHaveClass('parent'); + expect(parent).toHaveClass('parent-add'); + expect(child).not.toHaveClass('ng-enter'); + expect(child).toHaveClass('ng-enter-prepare'); + + $animate.flush(); + expect(parent).toHaveClass('parent parent-add parent-add-active'); + expect(child).toHaveClass('ng-enter ng-enter-active'); + expect(child).not.toHaveClass('ng-enter-prepare'); + }); + }); + + it('should pack level elements into their own RAF flush', function() { module('ngAnimateMock'); inject(function($animate, $compile, $rootScope, $rootElement, $document) {