fix(animations): ensure consistent transition namespace ordering (#19854)

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
inserted 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.

This commit fixes the issue by not just looking at the existence of a
parent node, but rather a more extensive check using the driver's
`containsElement` method.

PR Close #19854
This commit is contained in:
JoostK 2017-10-22 15:40:33 +02:00 committed by Misko Hevery
parent 35450c78f7
commit e27ac018ed
2 changed files with 87 additions and 2 deletions

View File

@ -571,7 +571,7 @@ export class TransitionAnimationEngine {
createNamespace(namespaceId: string, hostElement: any) {
const ns = new AnimationTransitionNamespace(namespaceId, hostElement, this);
if (hostElement.parentNode) {
if (this.bodyNode && this.driver.containsElement(this.bodyNode, hostElement)) {
this._balanceNamespaceList(ns, hostElement);
} else {
// defer this later until flush during when the host element has
@ -580,7 +580,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

View File

@ -2374,6 +2374,91 @@ describe('animation query tests', function() {
]);
});
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: `
<div @myAnimation *ngIf="exp" class="parent">
<child-cmp></child-cmp>
</div>
`,
animations: [
trigger(
'myAnimation',
[
transition(
':leave',
[
query('@*', animateChild()),
]),
]),
]
})
class ParentCmp {
public exp: boolean = true;
}
@Component({
selector: 'child-cmp',
template: `
<nested-child-cmp></nested-child-cmp>
`
})
class ChildCmp {
}
@Component({
selector: 'nested-child-cmp',
template: `
<section>
<div class="inner-div" @myChildAnimation></div>
</section>
`,
animations: [
trigger(
'myChildAnimation',
[
transition(
':leave',
[
style({opacity: 0}),
animate('1s', style({opacity: 1})),
]),
]),
]
})
class NestedChildCmp {
}
TestBed.configureTestingModule({declarations: [ParentCmp, ChildCmp, NestedChildCmp]});
const engine = TestBed.inject(ɵAnimationEngine);
const fixture = TestBed.createComponent(ParentCmp);
const cmp = fixture.componentInstance;
cmp.exp = true;
fixture.detectChanges();
cmp.exp = false;
fixture.detectChanges();
// Inspect the players of the AnimationEngine and not those from getLog. The latter only
// returns the actual animation players, which the parent leave animation is not part
// of given that it does not have animation instructions of its own.
const players = engine.players;
expect(players.length).toEqual(1);
const player = players[0] as TransitionAnimationPlayer;
const realPlayer = player.getRealPlayer() as MockAnimationPlayer;
expect(player.element.classList.contains('parent')).toBeTruthy();
expect(realPlayer.element.classList.contains('inner-div')).toBeTruthy();
expect(realPlayer.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({