diff --git a/packages/animations/browser/src/render/transition_animation_engine.ts b/packages/animations/browser/src/render/transition_animation_engine.ts index c7859fb475..b483cbc219 100644 --- a/packages/animations/browser/src/render/transition_animation_engine.ts +++ b/packages/animations/browser/src/render/transition_animation_engine.ts @@ -312,9 +312,13 @@ export class AnimationTransitionNamespace { // If there are no animations found for any of the nodes then clear the cache // for the element. this._engine.driver.query(rootElement, NG_TRIGGER_SELECTOR, true).forEach(elm => { + // this means that an inner remove() operation has already kicked off + // the animation on this element... + if (elm[REMOVAL_FLAG]) return; + const namespaces = this._engine.fetchNamespacesByElement(elm); if (namespaces.size) { - namespaces.forEach(ns => { ns.triggerLeaveAnimation(elm, context, false, true); }); + namespaces.forEach(ns => ns.triggerLeaveAnimation(elm, context, false, true)); } else { this.clearElementCache(elm); } diff --git a/packages/core/test/animation/animation_router_integration_spec.ts b/packages/core/test/animation/animation_router_integration_spec.ts index 545150fe19..e3c8dec217 100644 --- a/packages/core/test/animation/animation_router_integration_spec.ts +++ b/packages/core/test/animation/animation_router_integration_spec.ts @@ -5,13 +5,13 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ -import {animate, animateChild, query, style, transition, trigger, ɵAnimationGroupPlayer as AnimationGroupPlayer} from '@angular/animations'; +import {animate, animateChild, group, query, sequence, style, transition, trigger, ɵAnimationGroupPlayer as AnimationGroupPlayer} from '@angular/animations'; import {AnimationDriver, ɵAnimationEngine} from '@angular/animations/browser'; import {MockAnimationDriver, MockAnimationPlayer} from '@angular/animations/browser/testing'; import {Component, HostBinding} from '@angular/core'; -import {TestBed, fakeAsync, tick} from '@angular/core/testing'; +import {TestBed, fakeAsync, flushMicrotasks, tick} from '@angular/core/testing'; import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; -import {Router, RouterOutlet} from '@angular/router'; +import {ActivatedRoute, Router, RouterOutlet} from '@angular/router'; import {RouterTestingModule} from '@angular/router/testing'; export function main() { @@ -19,7 +19,14 @@ export function main() { if (typeof Element == 'undefined') return; describe('Animation Router Tests', function() { + function getLog(): MockAnimationPlayer[] { + return MockAnimationDriver.log as MockAnimationPlayer[]; + } + + function resetLog() { MockAnimationDriver.log = []; } + beforeEach(() => { + resetLog(); TestBed.configureTestingModule({ imports: [RouterTestingModule, BrowserAnimationsModule], providers: [{provide: AnimationDriver, useClass: MockAnimationDriver}] @@ -437,6 +444,80 @@ export function main() { expect(ip1.element.innerText).toEqual('page1'); expect(ip2.element.innerText).toEqual('page2'); })); + + it('should allow a recursive set of :leave animations to occur for nested routes', + fakeAsync(() => { + @Component({selector: 'ani-cmp', template: ''}) + class ContainerCmp { + constructor(private _router: Router) {} + log: string[] = []; + + enter() { this._router.navigateByUrl('/(recur:recur/nested)'); } + + leave() { this._router.navigateByUrl('/'); } + } + + @Component({ + selector: 'recur-page', + template: 'Depth: {{ depth }} \n ', + animations: [ + trigger( + 'pageAnimations', + [ + transition(':leave', [group([ + sequence([style({opacity: 1}), animate('1s', style({opacity: 0}))]), + query('@*', animateChild(), {optional: true}) + ])]), + ]), + ] + }) + class RecurPageCmp { + @HostBinding('@pageAnimations') public animatePage = true; + + @HostBinding('attr.data-depth') public depth = 0; + + constructor(private container: ContainerCmp, private route: ActivatedRoute) { + this.route.data.subscribe(data => { + this.container.log.push(`DEPTH ${data.depth}`); + this.depth = data.depth; + }); + } + } + + TestBed.configureTestingModule({ + declarations: [ContainerCmp, RecurPageCmp], + imports: [RouterTestingModule.withRoutes([{ + path: 'recur', + component: RecurPageCmp, + outlet: 'recur', + data: {depth: 0}, + children: [{path: 'nested', component: RecurPageCmp, data: {depth: 1}}] + }])] + }); + + const fixture = TestBed.createComponent(ContainerCmp); + const cmp = fixture.componentInstance; + cmp.enter(); + tick(); + fixture.detectChanges(); + flushMicrotasks(); + + expect(cmp.log).toEqual([ + 'DEPTH 0', + 'DEPTH 1', + ]); + + cmp.leave(); + tick(); + fixture.detectChanges(); + + const players = getLog(); + expect(players.length).toEqual(2); + + const [p1, p2] = players; + expect(p1.element.getAttribute('data-depth')).toEqual('0'); + expect(p2.element.getAttribute('data-depth')).toEqual('1'); + })); }); }