From 4311e4d4887957417ca582ba3c817f4857721bf8 Mon Sep 17 00:00:00 2001 From: JoostK Date: Sun, 22 Oct 2017 15:40:33 +0200 Subject: [PATCH] fix(animations): ensure consistent transition namespace ordering When including a component in a template, the component's host element is immediately appended as child of the parent node upon creation. Hence, `hostElement.parentNode` will be a valid reference. However, if the parent component is being inserted as an embedded view---through `ngIf` for example---then the parent's node itself will not have been insterted yet. This means that we cannot properly determine the position of the transition namespace, as any `containsElement` check will return false given that the partial DOM tree has not been inserted yet, even though it will be contained within an existing transition namespace once the partial tree is attached. --- .../src/render/transition_animation_engine.ts | 5 +- .../animation_query_integration_spec.ts | 81 +++++++++++++++++++ 2 files changed, 84 insertions(+), 2 deletions(-) diff --git a/packages/animations/browser/src/render/transition_animation_engine.ts b/packages/animations/browser/src/render/transition_animation_engine.ts index 2d1222936aad3f..6c1944686ad788 100644 --- a/packages/animations/browser/src/render/transition_animation_engine.ts +++ b/packages/animations/browser/src/render/transition_animation_engine.ts @@ -548,7 +548,8 @@ export class TransitionAnimationEngine { createNamespace(namespaceId: string, hostElement: any) { const ns = new AnimationTransitionNamespace(namespaceId, hostElement, this); - if (hostElement.parentNode) { + const bodyNode = getBodyNode(); + if (bodyNode && this.driver.containsElement(bodyNode, hostElement)) { this._balanceNamespaceList(ns, hostElement); } else { // defer this later until flush during when the host element has @@ -557,7 +558,7 @@ export class TransitionAnimationEngine { this.newHostElements.set(hostElement, ns); // given that this host element is apart of the animation code, it - // may or may not be inserted by a parent node that is an of an + // may or may not be inserted by a parent node that is of an // animation renderer type. If this happens then we can still have // access to this item when we query for :enter nodes. If the parent // is a renderer then the set data-structure will normalize the entry diff --git a/packages/core/test/animation/animation_query_integration_spec.ts b/packages/core/test/animation/animation_query_integration_spec.ts index a0d63325c44e6a..57e9fa01b4637b 100644 --- a/packages/core/test/animation/animation_query_integration_spec.ts +++ b/packages/core/test/animation/animation_query_integration_spec.ts @@ -2382,6 +2382,87 @@ import {fakeAsync, flushMicrotasks} from '../../testing/src/fake_async'; ]); }); + it('should emulate a leave animation on a nested sub component\'s inner elements when a parent leave animation occurs with animateChild', + () => { + @Component({ + selector: 'ani-cmp', + template: ` +
+ +
+ `, + animations: [ + trigger( + 'myAnimation', + [ + transition( + ':leave', + [ + query('@*', animateChild()), + ]), + ]), + ] + }) + class ParentCmp { + public exp: boolean = true; + } + + @Component({ + selector: 'child-cmp', + template: ` + + ` + }) + class ChildCmp { + } + + @Component({ + selector: 'nested-child-cmp', + template: ` +
+
+
+ `, + animations: [ + trigger( + 'myChildAnimation', + [ + transition( + ':leave', + [ + style({opacity: 0}), + animate('1s', style({opacity: 1})), + ]), + ]), + ] + }) + class NestedChildCmp { + } + + TestBed.configureTestingModule({declarations: [ParentCmp, ChildCmp, NestedChildCmp]}); + + const engine = TestBed.get(ɵAnimationEngine); + const fixture = TestBed.createComponent(ParentCmp); + const cmp = fixture.componentInstance; + + cmp.exp = true; + fixture.detectChanges(); + + cmp.exp = false; + fixture.detectChanges(); + + let players = engine.players; + expect(players.length).toEqual(1); + const [player] = players; + + expect(player.element.classList.contains('parent')).toBeTruthy(); + expect(player.getRealPlayer().element.classList.contains('inner-div')).toBeTruthy(); + expect(player.getRealPlayer().keyframes).toEqual([ + {opacity: '0', offset: 0}, + {opacity: '1', offset: 1}, + ]); + }); + it('should not cause a removal of inner @trigger DOM nodes when a parent animation occurs', fakeAsync(() => { @Component({