From 6e18b50a5b168848cc526081b0a2a16075ee44bd Mon Sep 17 00:00:00 2001 From: Martin Staffa Date: Thu, 3 Dec 2015 15:12:30 +0100 Subject: [PATCH] feat(ngAnimate): provide ng-[event]-prepare class for structural animations The new prepare class is added before the animation is pushed to the queue and removed before the animation runs, i.e. it is immediately available when a structural animation (enter, leave, move) is initialized. The class can be used to apply CSS to explicitly hide these elements to prevent a flash of content before the animation runs. This can happen if a structural animation (such as ng-if) sits at the bottom of a tree which has ng-class animations on the parents. Because child animations are spaced out with requestAnimationFrame, the ng-enter class might not be applied in time, so the ng.if element is briefly visible before its animation starts. --- docs/content/guide/animations.ngdoc | 31 +++++++++++++++++++++++++ src/ngAnimate/.jshintrc | 1 + src/ngAnimate/animation.js | 10 ++++++++ src/ngAnimate/module.js | 28 ++++++++++++++++++++++ src/ngAnimate/shared.js | 1 + test/ngAnimate/animationSpec.js | 19 +++++++++++++++ test/ngAnimate/integrationSpec.js | 36 +++++++++++++++++++++++++++++ 7 files changed, 126 insertions(+) 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) {