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 (#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.
  • Loading branch information
crisbeto authored and jelbourn committed Dec 14, 2016
1 parent 8df30db commit db9608f
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._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();
}
}

Expand All @@ -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();
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 db9608f

Please sign in to comment.