diff --git a/src/lib/snack-bar/snack-bar-container.ts b/src/lib/snack-bar/snack-bar-container.ts index 765bc5b18793..e617b56d30ea 100644 --- a/src/lib/snack-bar/snack-bar-container.ts +++ b/src/lib/snack-bar/snack-bar-container.ts @@ -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 { - 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 { + this.animationState = 'visible'; return this.onEnter.asObservable(); } + + /** Begin animation of the snack bar exiting from view. */ + exit(): Observable { + this.animationState = 'complete'; + return this._onExit(); + } + + /** Returns an observable that completes after the closing animation is done. */ + _onExit(): Observable { + 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(); + }); + } } diff --git a/src/lib/snack-bar/snack-bar-ref.ts b/src/lib/snack-bar/snack-bar-ref.ts index 21cb7e8bc4a5..eb9e3b84b146 100644 --- a/src/lib/snack-bar/snack-bar-ref.ts +++ b/src/lib/snack-bar/snack-bar-ref.ts @@ -33,16 +33,13 @@ export class MdSnackBarRef { this.containerInstance = containerInstance; // Dismiss snackbar on action. this.onAction().subscribe(() => this.dismiss()); + containerInstance._onExit().subscribe(() => this._cleanup()); } /** 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 { } } + /** Cleans up the DOM after closing. */ + private _cleanup(): 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 { return this._afterClosed.asObservable(); diff --git a/src/lib/snack-bar/snack-bar.spec.ts b/src/lib/snack-bar/snack-bar.spec.ts index 9edb98dd98be..773f4a62d880 100644 --- a/src/lib/snack-bar/snack-bar.spec.ts +++ b/src/lib/snack-bar/snack-bar.spec.ts @@ -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: ``, + template: ``, }) 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],