Skip to content

Commit

Permalink
fix(animations): Trigger leave animation when ViewContainerRef is inj…
Browse files Browse the repository at this point in the history
…ected

Injecting `ViewContainerRef` into a component makes it effectively a container. The leave animation wasn't triggered on containers before this fix.

fixes #48667
  • Loading branch information
JeanMeche committed May 16, 2023
1 parent b6e3840 commit c4e8fbd
Show file tree
Hide file tree
Showing 5 changed files with 60 additions and 12 deletions.
Expand Up @@ -72,8 +72,8 @@ export class AnimationEngine {
this._transitionEngine.insertNode(namespaceId, element, parent, insertBefore);
}

onRemove(namespaceId: string, element: any, context: any, isHostElement?: boolean): void {
this._transitionEngine.removeNode(namespaceId, element, isHostElement || false, context);
onRemove(namespaceId: string, element: any, context: any): void {
this._transitionEngine.removeNode(namespaceId, element, context);
}

disableAnimations(element: any, disable: boolean) {
Expand Down
Expand Up @@ -744,7 +744,7 @@ export class TransitionAnimationEngine {
}
}

removeNode(namespaceId: string, element: any, isHostElement: boolean, context: any): void {
removeNode(namespaceId: string, element: any, context: any): void {
if (isElementNode(element)) {
const ns = namespaceId ? this._fetchNamespace(namespaceId) : null;
if (ns) {
Expand All @@ -753,11 +753,9 @@ export class TransitionAnimationEngine {
this.markElementAsRemoved(namespaceId, element, false, context);
}

if (isHostElement) {
const hostNS = this.namespacesByHostElement.get(element);
if (hostNS && hostNS.id !== namespaceId) {
hostNS.removeNode(element, context);
}
const hostNS = this.namespacesByHostElement.get(element);
if (hostNS && hostNS.id !== namespaceId) {
hostNS.removeNode(element, context);
}
} else {
this._onRemovalComplete(element, context);
Expand Down
Expand Up @@ -116,7 +116,7 @@ describe('TransitionAnimationEngine', () => {

expect(engine.elementContainsData(DEFAULT_NAMESPACE_ID, element)).toBeTruthy();

engine.removeNode(DEFAULT_NAMESPACE_ID, element, true, true);
engine.removeNode(DEFAULT_NAMESPACE_ID, element, true);
engine.flush();

expect(engine.elementContainsData(DEFAULT_NAMESPACE_ID, element)).toBeTruthy();
Expand Down Expand Up @@ -171,7 +171,7 @@ describe('TransitionAnimationEngine', () => {
expect(engine.statesByElement.has(element)).toBe(true, 'Expected parent data to be defined.');
expect(engine.statesByElement.has(child)).toBe(true, 'Expected child data to be defined.');

engine.removeNode(DEFAULT_NAMESPACE_ID, element, true, true);
engine.removeNode(DEFAULT_NAMESPACE_ID, element, true);
engine.flush();
engine.players[0].finish();

Expand Down
52 changes: 51 additions & 1 deletion packages/core/test/animation/animation_integration_spec.ts
Expand Up @@ -8,7 +8,7 @@
import {animate, animateChild, animation, AnimationEvent, AnimationMetadata, AnimationOptions, AUTO_STYLE, group, keyframes, query, state, style, transition, trigger, useAnimation, ɵPRE_STYLE as PRE_STYLE} from '@angular/animations';
import {AnimationDriver, ɵAnimationEngine, ɵNoopAnimationDriver as NoopAnimationDriver} from '@angular/animations/browser';
import {MockAnimationDriver, MockAnimationPlayer} from '@angular/animations/browser/testing';
import {ChangeDetectorRef, Component, HostBinding, HostListener, Inject, RendererFactory2, ViewChild} from '@angular/core';
import {ChangeDetectorRef, Component, HostBinding, HostListener, Inject, RendererFactory2, ViewChild, ViewContainerRef} from '@angular/core';
import {fakeAsync, flushMicrotasks, TestBed} from '@angular/core/testing';
import {ɵDomRendererFactory2} from '@angular/platform-browser';
import {ANIMATION_MODULE_TYPE, BrowserAnimationsModule, NoopAnimationsModule} from '@angular/platform-browser/animations';
Expand Down Expand Up @@ -909,6 +909,56 @@ describe('animation tests', function() {
]);
}));

it('should trigger a leave animation when the inner has ViewContainerRef injected',
fakeAsync(() => {
@Component({
selector: 'parent-cmp',
template: `
<child-cmp *ngIf="exp"></child-cmp>
`
})
class ParentCmp {
public exp = true;
}

@Component({
selector: 'child-cmp',
template: '...',
animations: [trigger(
'host',
[transition(':leave', [style({opacity: 1}), animate(1000, style({opacity: 0}))])])]
})
class ChildCmp {
@HostBinding('@host') public hostAnimation = true;
constructor(private vcr: ViewContainerRef) {}
}

TestBed.configureTestingModule({declarations: [ParentCmp, ChildCmp]});

const engine = TestBed.inject(ɵAnimationEngine);
const fixture = TestBed.createComponent(ParentCmp);
const cmp = fixture.componentInstance;
fixture.detectChanges();
engine.flush();
expect(getLog().length).toEqual(0);

cmp.exp = false;
fixture.detectChanges();
expect(fixture.debugElement.nativeElement.children.length).toBe(1);

engine.flush();
expect(getLog().length).toEqual(1);

const [player] = getLog();
expect(player.keyframes).toEqual([
new Map<string, string|number>([['opacity', '1'], ['offset', 0]]),
new Map<string, string|number>([['opacity', '0'], ['offset', 1]]),
]);

player.finish();
expect(fixture.debugElement.nativeElement.children.length).toBe(0);
}));

it('should trigger a leave animation when the inner components host binding updates',
fakeAsync(() => {
@Component({
Expand Down
Expand Up @@ -180,7 +180,7 @@ export class BaseAnimationRenderer implements Renderer2 {
}

removeChild(parent: any, oldChild: any, isHostElement: boolean): void {
this.engine.onRemove(this.namespaceId, oldChild, this.delegate, isHostElement);
this.engine.onRemove(this.namespaceId, oldChild, this.delegate);
}

selectRootElement(selectorOrNode: any, preserveContent?: boolean) {
Expand Down

0 comments on commit c4e8fbd

Please sign in to comment.