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');
+ }));
});
}