diff --git a/packages/core/test/animation/animation_integration_spec.ts b/packages/core/test/animation/animation_integration_spec.ts index dbaee6ecd9..7e089d48fc 100644 --- a/packages/core/test/animation/animation_integration_spec.ts +++ b/packages/core/test/animation/animation_integration_spec.ts @@ -8,7 +8,7 @@ import {AUTO_STYLE, AnimationEvent, AnimationOptions, animate, animateChild, group, keyframes, query, state, style, transition, trigger, ɵPRE_STYLE as PRE_STYLE} from '@angular/animations'; import {AnimationDriver, ɵAnimationEngine, ɵNoopAnimationDriver} from '@angular/animations/browser'; import {MockAnimationDriver, MockAnimationPlayer} from '@angular/animations/browser/testing'; -import {Component, HostBinding, HostListener, RendererFactory2, ViewChild} from '@angular/core'; +import {ChangeDetectorRef, Component, HostBinding, HostListener, RendererFactory2, ViewChild} from '@angular/core'; import {ɵDomRendererFactory2} from '@angular/platform-browser'; import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter'; @@ -1482,6 +1482,65 @@ export function main() { ]); }); + it('should not flush animations twice when an inner component runs change detection', () => { + @Component({ + selector: 'outer-cmp', + template: ` +
+ + `, + animations: [trigger( + 'outer', + [transition(':enter', [style({opacity: 0}), animate('1s', style({opacity: 1}))])])] + }) + class OuterCmp { + @ViewChild('inner') public inner: any; + public exp: any = null; + + update() { this.exp = 'go'; } + + ngDoCheck() { + if (this.exp == 'go') { + this.inner.update(); + } + } + } + + @Component({ + selector: 'inner-cmp', + template: ` +
+ `, + animations: [trigger('inner', [transition( + ':enter', + [ + style({opacity: 0}), + animate('1s', style({opacity: 1})), + ])])] + }) + class InnerCmp { + public exp: any; + constructor(private _ref: ChangeDetectorRef) {} + update() { + this.exp = 'go'; + this._ref.detectChanges(); + } + } + + TestBed.configureTestingModule({declarations: [OuterCmp, InnerCmp]}); + + const engine = TestBed.get(ɵAnimationEngine); + const fixture = TestBed.createComponent(OuterCmp); + const cmp = fixture.componentInstance; + fixture.detectChanges(); + expect(getLog()).toEqual([]); + + cmp.update(); + fixture.detectChanges(); + + const players = getLog(); + expect(players.length).toEqual(2); + }); }); describe('animation listeners', () => { diff --git a/packages/platform-browser/animations/src/animation_renderer.ts b/packages/platform-browser/animations/src/animation_renderer.ts index 3f57a5b9ab..4d3adaf24d 100644 --- a/packages/platform-browser/animations/src/animation_renderer.ts +++ b/packages/platform-browser/animations/src/animation_renderer.ts @@ -15,6 +15,7 @@ export class AnimationRendererFactory implements RendererFactory2 { private _microtaskId: number = 1; private _animationCallbacksBuffer: [(e: any) => any, any][] = []; private _rendererCache = new Map(); + private _cdRecurDepth = 0; constructor( private delegate: RendererFactory2, private engine: AnimationEngine, private _zone: NgZone) { @@ -58,6 +59,7 @@ export class AnimationRendererFactory implements RendererFactory2 { } begin() { + this._cdRecurDepth++; if (this.delegate.begin) { this.delegate.begin(); } @@ -90,10 +92,16 @@ export class AnimationRendererFactory implements RendererFactory2 { } end() { - this._zone.runOutsideAngular(() => { - this._scheduleCountTask(); - this.engine.flush(this._microtaskId); - }); + this._cdRecurDepth--; + + // this is to prevent animations from running twice when an inner + // component does CD when a parent component insted has inserted it + if (this._cdRecurDepth == 0) { + this._zone.runOutsideAngular(() => { + this._scheduleCountTask(); + this.engine.flush(this._microtaskId); + }); + } if (this.delegate.end) { this.delegate.end(); }