Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.

Commit 6e18b50

Browse files
committed
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.
1 parent 77419cf commit 6e18b50

File tree

7 files changed

+126
-0
lines changed

7 files changed

+126
-0
lines changed

docs/content/guide/animations.ngdoc

+31
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,37 @@ myModule.directive('my-directive', ['$animate', function($animate) {
274274
}]);
275275
```
276276

277+
## Preventing flicker before an animation starts
278+
279+
When nesting elements with structural animations such as `ngIf` into elements that have class-based
280+
animations such as `ngClass`, it sometimes happens that before the actual animation starts, there is a brief flicker or flash of content
281+
where the animated element is briefly visible.
282+
283+
To prevent this, you can apply styles to the `ng-[event]-prepare` class, which is added as soon as an animation is initialized,
284+
but removed before the actual animation starts (after waiting for a $digest). This class is only added for *structural*
285+
animations (`enter`, `move`, and `leave`).
286+
287+
Here's an example where you might see flickering:
288+
289+
```html
290+
<div ng-class="{red: myProp}">
291+
<div ng-class="{blue: myProp}">
292+
<div class="message" ng-if="myProp"></div>
293+
</div>
294+
</div>
295+
```
296+
297+
It is possible that during the `enter` event, the `.message` div will be briefly visible before it starts animating.
298+
In that case, you can add styles to the CSS that make sure the element stays hidden before the animation starts:
299+
300+
```css
301+
.message.ng-enter-prepare {
302+
opacity: 0;
303+
}
304+
305+
/* Other animation styles ... */
306+
```
307+
277308
## More about animations
278309

279310
For a full breakdown of each method available on `$animate`, see the {@link ng.$animate API documentation}.

src/ngAnimate/.jshintrc

+1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
"REMOVE_CLASS_SUFFIX": false,
3030
"EVENT_CLASS_PREFIX": false,
3131
"ACTIVE_CLASS_SUFFIX": false,
32+
"PREPARE_CLASS_SUFFIX": false,
3233

3334
"TRANSITION_DURATION_PROP": false,
3435
"TRANSITION_DELAY_PROP": false,

src/ngAnimate/animation.js

+10
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,12 @@ var $$AnimationProvider = ['$animateProvider', function($animateProvider) {
135135
options.tempClasses = null;
136136
}
137137

138+
var prepareClassName;
139+
if (isStructural) {
140+
prepareClassName = 'ng-' + event + PREPARE_CLASS_SUFFIX;
141+
$$jqLite.addClass(element, prepareClassName);
142+
}
143+
138144
animationQueue.push({
139145
// this data is used by the postDigest code and passed into
140146
// the driver step function
@@ -357,6 +363,10 @@ var $$AnimationProvider = ['$animateProvider', function($animateProvider) {
357363
if (tempClasses) {
358364
$$jqLite.addClass(element, tempClasses);
359365
}
366+
if (prepareClassName) {
367+
$$jqLite.removeClass(element, prepareClassName);
368+
prepareClassName = null;
369+
}
360370
}
361371

362372
function updateAnimationRunners(animation, newRunner) {

src/ngAnimate/module.js

+28
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,34 @@
254254
* the CSS class once an animation has completed.)
255255
*
256256
*
257+
* ### The `ng-[event]-prepare` class
258+
*
259+
* This is a special class that can be used to prevent unwanted flickering / flash of content before
260+
* the actual animation starts. The class is added as soon as an animation is initialized, but removed
261+
* before the actual animation starts (after waiting for a $digest).
262+
* It is also only added for *structural* animations (`enter`, `move`, and `leave`).
263+
*
264+
* In practice, flickering can appear when nesting elements with structural animations such as `ngIf`
265+
* into elements that have class-based animations such as `ngClass`.
266+
*
267+
* ```html
268+
* <div ng-class="{red: myProp}">
269+
* <div ng-class="{blue: myProp}">
270+
* <div class="message" ng-if="myProp"></div>
271+
* </div>
272+
* </div>
273+
* ```
274+
*
275+
* It is possible that during the `enter` animation, the `.message` div will be briefly visible before it starts animating.
276+
* In that case, you can add styles to the CSS that make sure the element stays hidden before the animation starts:
277+
*
278+
* ```css
279+
* .message.ng-enter-prepare {
280+
* opacity: 0;
281+
* }
282+
*
283+
* ```
284+
*
257285
* ## JavaScript-based Animations
258286
*
259287
* ngAnimate also allows for animations to be consumed by JavaScript code. The approach is similar to CSS-based animations (where there is a shared

src/ngAnimate/shared.js

+1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ var ADD_CLASS_SUFFIX = '-add';
2121
var REMOVE_CLASS_SUFFIX = '-remove';
2222
var EVENT_CLASS_PREFIX = 'ng-';
2323
var ACTIVE_CLASS_SUFFIX = '-active';
24+
var PREPARE_CLASS_SUFFIX = '-prepare';
2425

2526
var NG_ANIMATE_CLASSNAME = 'ng-animate';
2627
var NG_ANIMATE_CHILDREN_DATA = '$$ngAnimateChildren';

test/ngAnimate/animationSpec.js

+19
Original file line numberDiff line numberDiff line change
@@ -513,6 +513,25 @@ describe('$$animation', function() {
513513
expect(captureLog[1].element).toBe(child);
514514
expect(captureLog[2].element).toBe(grandchild);
515515
}));
516+
517+
518+
they('should add the preparation class before the $prop-animation is pushed to the queue',
519+
['enter', 'leave', 'move'], function(animationType) {
520+
inject(function($$animation, $rootScope, $animate) {
521+
var runner = $$animation(element, animationType);
522+
expect(element).toHaveClass('ng-' + animationType + '-prepare');
523+
});
524+
});
525+
526+
527+
they('should remove the preparation class before the $prop-animation starts',
528+
['enter', 'leave', 'move'], function(animationType) {
529+
inject(function($$animation, $rootScope, $$rAF) {
530+
var runner = $$animation(element, animationType);
531+
$rootScope.$digest();
532+
expect(element).not.toHaveClass('ng-' + animationType + '-prepare');
533+
});
534+
});
516535
});
517536

518537
describe("grouped", function() {

test/ngAnimate/integrationSpec.js

+36
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,42 @@ describe('ngAnimate integration tests', function() {
268268
});
269269
});
270270

271+
it('should add the preparation class for an enter animation before a parent class-based animation is applied', function() {
272+
module('ngAnimateMock');
273+
inject(function($animate, $compile, $rootScope, $rootElement, $document) {
274+
element = jqLite(
275+
'<div ng-class="{parent:exp}">' +
276+
'<div ng-if="exp">' +
277+
'</div>' +
278+
'</div>'
279+
);
280+
281+
ss.addRule('.ng-enter', 'transition:2s linear all;');
282+
ss.addRule('.parent-add', 'transition:5s linear all;');
283+
284+
$rootElement.append(element);
285+
jqLite($document[0].body).append($rootElement);
286+
287+
$compile(element)($rootScope);
288+
$rootScope.exp = true;
289+
$rootScope.$digest();
290+
291+
var parent = element;
292+
var child = element.find('div');
293+
294+
expect(parent).not.toHaveClass('parent');
295+
expect(parent).toHaveClass('parent-add');
296+
expect(child).not.toHaveClass('ng-enter');
297+
expect(child).toHaveClass('ng-enter-prepare');
298+
299+
$animate.flush();
300+
expect(parent).toHaveClass('parent parent-add parent-add-active');
301+
expect(child).toHaveClass('ng-enter ng-enter-active');
302+
expect(child).not.toHaveClass('ng-enter-prepare');
303+
});
304+
});
305+
306+
271307
it('should pack level elements into their own RAF flush', function() {
272308
module('ngAnimateMock');
273309
inject(function($animate, $compile, $rootScope, $rootElement, $document) {

0 commit comments

Comments
 (0)