From a999772f9f8b9531306a29d287aff2a82ecffa64 Mon Sep 17 00:00:00 2001 From: crisbeto Date: Tue, 15 Nov 2016 22:28:34 +0100 Subject: [PATCH] fix(tab-link): avoid potential memory leak * Similarly to #1838, the `tab-link` destroy handler may not be called in certain situations, because it is being inherited from the MdRipple class. This PR explicitly calls the destroy handler. * Adds a unit test to make sure that the ripples are being cleaned up properly. --- src/lib/core/ripple/ripple.spec.ts | 26 ++++++++++++++- src/lib/tabs/tab-nav-bar/tab-nav-bar.spec.ts | 33 +++++++++++++++++++- src/lib/tabs/tab-nav-bar/tab-nav-bar.ts | 19 +++++++++-- 3 files changed, 74 insertions(+), 4 deletions(-) diff --git a/src/lib/core/ripple/ripple.spec.ts b/src/lib/core/ripple/ripple.spec.ts index 422f908a0ba2..3f38c63e31cd 100644 --- a/src/lib/core/ripple/ripple.spec.ts +++ b/src/lib/core/ripple/ripple.spec.ts @@ -64,7 +64,11 @@ describe('MdRipple', () => { beforeEach(() => { TestBed.configureTestingModule({ imports: [MdRippleModule.forRoot()], - declarations: [BasicRippleContainer, RippleContainerWithInputBindings], + declarations: [ + BasicRippleContainer, + RippleContainerWithInputBindings, + RippleContainerWithNgIf, + ], }); }); @@ -180,6 +184,20 @@ describe('MdRipple', () => { expect(pxStringToFloat(ripple.style.width)).toBeCloseTo(2 * expectedRadius, 1); expect(pxStringToFloat(ripple.style.height)).toBeCloseTo(2 * expectedRadius, 1); }); + + it('cleans up the event handlers when the container gets destroyed', () => { + fixture = TestBed.createComponent(RippleContainerWithNgIf); + fixture.detectChanges(); + + rippleElement = fixture.debugElement.nativeElement.querySelector('[md-ripple]'); + rippleBackground = rippleElement.querySelector('.md-ripple-background'); + + fixture.componentInstance.isDestroyed = true; + fixture.detectChanges(); + + rippleElement.dispatchEvent(createMouseEvent('mousedown')); + expect(rippleBackground.classList).not.toContain('md-ripple-active'); + }); }); describe('configuring behavior', () => { @@ -328,3 +346,9 @@ class RippleContainerWithInputBindings { backgroundColor = ''; @ViewChild(MdRipple) ripple: MdRipple; } + +@Component({ template: `
` }) +class RippleContainerWithNgIf { + @ViewChild(MdRipple) ripple: MdRipple; + isDestroyed = false; +} diff --git a/src/lib/tabs/tab-nav-bar/tab-nav-bar.spec.ts b/src/lib/tabs/tab-nav-bar/tab-nav-bar.spec.ts index a2074c18fdf5..9cd316a89ab3 100644 --- a/src/lib/tabs/tab-nav-bar/tab-nav-bar.spec.ts +++ b/src/lib/tabs/tab-nav-bar/tab-nav-bar.spec.ts @@ -10,7 +10,8 @@ describe('MdTabNavBar', () => { TestBed.configureTestingModule({ imports: [MdTabsModule.forRoot()], declarations: [ - SimpleTabNavBarTestApp + SimpleTabNavBarTestApp, + TabLinkWithNgIf, ], }); @@ -38,6 +39,25 @@ describe('MdTabNavBar', () => { expect(component.activeIndex).toBe(2); }); }); + + it('should clean up the ripple event handlers on destroy', () => { + let fixture: ComponentFixture = TestBed.createComponent(TabLinkWithNgIf); + fixture.detectChanges(); + + let link = fixture.debugElement.nativeElement.querySelector('[md-tab-link]'); + let rippleBackground = link.querySelector('.md-ripple-background'); + let mouseEvent = document.createEvent('MouseEvents'); + + fixture.componentInstance.isDestroyed = true; + fixture.detectChanges(); + + mouseEvent.initMouseEvent('mousedown', false, false, window, 0, 0, 0, 0, 0, false, false, + false, false, 0, null); + + link.dispatchEvent(mouseEvent); + + expect(rippleBackground.classList).not.toContain('md-ripple-active'); + }); }); @Component({ @@ -53,3 +73,14 @@ describe('MdTabNavBar', () => { class SimpleTabNavBarTestApp { activeIndex = 0; } + +@Component({ + template: ` + + ` +}) +class TabLinkWithNgIf { + isDestroyed = false; +} diff --git a/src/lib/tabs/tab-nav-bar/tab-nav-bar.ts b/src/lib/tabs/tab-nav-bar/tab-nav-bar.ts index 1e7ab6f7e90b..365336b5c54d 100644 --- a/src/lib/tabs/tab-nav-bar/tab-nav-bar.ts +++ b/src/lib/tabs/tab-nav-bar/tab-nav-bar.ts @@ -1,4 +1,13 @@ -import {Component, Input, ViewChild, ElementRef, ViewEncapsulation, Directive} from '@angular/core'; +import { + Component, + Input, + ViewChild, + ElementRef, + ViewEncapsulation, + Directive, + OnDestroy, +} from '@angular/core'; + import {MdInkBar} from '../ink-bar'; import {MdRipple} from '../../core/ripple/ripple'; @@ -50,8 +59,14 @@ export class MdTabLink { @Directive({ selector: '[md-tab-link]', }) -export class MdTabLinkRipple extends MdRipple { +export class MdTabLinkRipple extends MdRipple implements OnDestroy { constructor(private _element: ElementRef) { super(_element); } + + // In certain cases the parent destroy handler + // may not get called. See Angular issue #11606. + ngOnDestroy() { + super.ngOnDestroy(); + } }