fix(snack-bar): clean up element when associated viewContainer is destroyed (#2219)

* fix(snack-bar): clean up element when associated viewContainer is destroyed

Fixes the snack bar not being removed from the DOM when it's associated `viewContainerRef` gets destroyed.

Fixes #2190.

* Fix bad formatting.

* Rename the cleanup method.
This commit is contained in:
Kristiyan Kostadinov
2016-12-14 23:32:13 +01:00
committed by Jeremy Elbourn
parent 8df30dbd81
commit db9608f875
3 changed files with 53 additions and 15 deletions
+25 -8
View File
@@ -8,7 +8,8 @@ import {
transition,
animate,
AnimationTransitionEvent,
NgZone
NgZone,
OnDestroy,
} from '@angular/core';
import {
BasePortalHost,
@@ -53,7 +54,7 @@ export const HIDE_ANIMATION = '195ms cubic-bezier(0.0,0.0,0.2,1)';
])
],
})
export class MdSnackBarContainer extends BasePortalHost {
export class MdSnackBarContainer extends BasePortalHost implements OnDestroy {
/** The portal host inside of this container into which the snack bar content will be loaded. */
@ViewChild(PortalHostDirective) _portalHost: PortalHostDirective;
@@ -87,12 +88,6 @@ export class MdSnackBarContainer extends BasePortalHost {
throw Error('Not yet implemented');
}
/** Begin animation of the snack bar exiting from view. */
exit(): Observable<void> {
this.animationState = 'complete';
return this.onExit.asObservable();
}
/** Handle end of animations, updating the state of the snackbar. */
onAnimationEnd(event: AnimationTransitionEvent) {
if (event.toState === 'void' || event.toState === 'complete') {
@@ -116,6 +111,28 @@ export class MdSnackBarContainer extends BasePortalHost {
/** Returns an observable resolving when the enter animation completes. */
_onEnter(): Observable<void> {
this.animationState = 'visible';
return this.onEnter.asObservable();
}
/** Begin animation of the snack bar exiting from view. */
exit(): Observable<void> {
this.animationState = 'complete';
return this._onExit();
}
/** Returns an observable that completes after the closing animation is done. */
_onExit(): Observable<void> {
return this.onExit.asObservable();
}
/** Makes sure the exit callbacks have been invoked when the element is destroyed. */
ngOnDestroy() {
// Wait for the zone to settle before removing the element. Helps prevent
// errors where we end up removing an element which is in the middle of an animation.
this._ngZone.onMicrotaskEmpty.first().subscribe(() => {
this.onExit.next();
this.onExit.complete();
});
}
}
+9 -5
View File
@@ -33,16 +33,13 @@ export class MdSnackBarRef<T> {
this.containerInstance = containerInstance;
// Dismiss snackbar on action.
this.onAction().subscribe(() => this.dismiss());
containerInstance._onExit().subscribe(() => this._finishDismiss());
}
/** Dismisses the snack bar. */
dismiss(): void {
if (!this._afterClosed.closed) {
this.containerInstance.exit().subscribe(() => {
this._overlayRef.dispose();
this._afterClosed.next();
this._afterClosed.complete();
});
this.containerInstance.exit();
}
}
@@ -62,6 +59,13 @@ export class MdSnackBarRef<T> {
}
}
/** Cleans up the DOM after closing. */
private _finishDismiss(): void {
this._overlayRef.dispose();
this._afterClosed.next();
this._afterClosed.complete();
}
/** Gets an observable that is notified when the snack bar is finished closing. */
afterDismissed(): Observable<void> {
return this._afterClosed.asObservable();
+19 -2
View File
@@ -8,6 +8,7 @@ import {
tick,
} from '@angular/core/testing';
import {NgModule, Component, Directive, ViewChild, ViewContainerRef} from '@angular/core';
import {CommonModule} from '@angular/common';
import {MdSnackBar, MdSnackBarModule} from './snack-bar';
import {MdSnackBarConfig} from './snack-bar-config';
import {OverlayContainer, MdLiveAnnouncer} from '../core';
@@ -151,6 +152,20 @@ describe('MdSnackBar', () => {
});
}));
it('should clean itself up when the view container gets destroyed', async(() => {
snackBar.open(simpleMessage, null, { viewContainerRef: testViewContainerRef });
viewContainerFixture.detectChanges();
expect(overlayContainerElement.childElementCount).toBeGreaterThan(0);
viewContainerFixture.componentInstance.childComponentExists = false;
viewContainerFixture.detectChanges();
viewContainerFixture.whenStable().then(() => {
expect(overlayContainerElement.childElementCount)
.toBe(0, 'Expected snack bar to be removed after the view container was destroyed');
});
}));
it('should open a custom component', () => {
let config = {viewContainerRef: testViewContainerRef};
let snackBarRef = snackBar.openFromComponent(BurritosNotification, config);
@@ -314,11 +329,13 @@ class DirectiveWithViewContainer {
@Component({
selector: 'arbitrary-component',
template: `<dir-with-view-container></dir-with-view-container>`,
template: `<dir-with-view-container *ngIf="childComponentExists"></dir-with-view-container>`,
})
class ComponentWithChildViewContainer {
@ViewChild(DirectiveWithViewContainer) childWithViewContainer: DirectiveWithViewContainer;
childComponentExists: boolean = true;
get childViewContainer() {
return this.childWithViewContainer.viewContainerRef;
}
@@ -337,7 +354,7 @@ const TEST_DIRECTIVES = [ComponentWithChildViewContainer,
BurritosNotification,
DirectiveWithViewContainer];
@NgModule({
imports: [MdSnackBarModule],
imports: [CommonModule, MdSnackBarModule],
exports: TEST_DIRECTIVES,
declarations: TEST_DIRECTIVES,
entryComponents: [ComponentWithChildViewContainer, BurritosNotification],