Skip to content

Commit

Permalink
fix(snack-bar): clean up element when associated viewContainer is des…
Browse files Browse the repository at this point in the history
…troyed

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

Fixes angular#2190.
  • Loading branch information
crisbeto committed Dec 14, 2016
1 parent a7c88c5 commit 68ebd3a
Show file tree
Hide file tree
Showing 3 changed files with 53 additions and 15 deletions.
33 changes: 25 additions & 8 deletions src/lib/snack-bar/snack-bar-container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import {
transition,
animate,
AnimationTransitionEvent,
NgZone
NgZone,
OnDestroy,
} from '@angular/core';
import {
BasePortalHost,
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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') {
Expand All @@ -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();
});
}
}
14 changes: 9 additions & 5 deletions src/lib/snack-bar/snack-bar-ref.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,16 +33,13 @@ export class MdSnackBarRef<T> {
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();
}
}

Expand All @@ -62,6 +59,13 @@ export class MdSnackBarRef<T> {
}
}

/** 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<void> {
return this._afterClosed.asObservable();
Expand Down
21 changes: 19 additions & 2 deletions src/lib/snack-bar/snack-bar.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
}
Expand All @@ -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],
Expand Down

0 comments on commit 68ebd3a

Please sign in to comment.