diff --git a/src/lib/core/overlay/position/viewport-ruler.ts b/src/lib/core/overlay/position/viewport-ruler.ts index d39271f75411..2df3fe3ed5b0 100644 --- a/src/lib/core/overlay/position/viewport-ruler.ts +++ b/src/lib/core/overlay/position/viewport-ruler.ts @@ -17,7 +17,7 @@ export class ViewportRuler { this._cacheViewportGeometry(); // Subscribe to scroll and resize events and update the document rectangle on changes. - scrollDispatcher.scrolled().subscribe(() => this._cacheViewportGeometry()); + scrollDispatcher.scrolled(null, () => this._cacheViewportGeometry()); } /** Gets a ClientRect for the viewport's bounds. */ diff --git a/src/lib/core/overlay/scroll/scroll-dispatcher.spec.ts b/src/lib/core/overlay/scroll/scroll-dispatcher.spec.ts index 77342af30f20..8b53de57666f 100644 --- a/src/lib/core/overlay/scroll/scroll-dispatcher.spec.ts +++ b/src/lib/core/overlay/scroll/scroll-dispatcher.spec.ts @@ -48,7 +48,7 @@ describe('Scroll Dispatcher', () => { // Listen for notifications from scroll service with a throttle of 100ms const throttleTime = 100; let hasServiceScrollNotified = false; - scroll.scrolled(throttleTime).subscribe(() => { hasServiceScrollNotified = true; }); + scroll.scrolled(throttleTime, () => { hasServiceScrollNotified = true; }); // Emit a scroll event from the scrolling element in our component. // This event should be picked up by the scrollable directive and notify. @@ -90,6 +90,29 @@ describe('Scroll Dispatcher', () => { expect(scrollableElementIds).toEqual(['scrollable-1', 'scrollable-1a']); }); }); + + describe('lazy subscription', () => { + let scroll: ScrollDispatcher; + + beforeEach(inject([ScrollDispatcher], (s: ScrollDispatcher) => { + scroll = s; + })); + + it('should lazily add global listeners as service subscriptions are added and removed', () => { + expect(scroll._globalSubscription).toBeNull('Expected no global listeners on init.'); + + const subscription = scroll.scrolled(0, () => {}); + + expect(scroll._globalSubscription).toBeTruthy( + 'Expected global listeners after a subscription has been added.'); + + subscription.unsubscribe(); + + expect(scroll._globalSubscription).toBeNull( + 'Expected global listeners to have been removed after the subscription has stopped.'); + }); + + }); }); diff --git a/src/lib/core/overlay/scroll/scroll-dispatcher.ts b/src/lib/core/overlay/scroll/scroll-dispatcher.ts index 334ebabfa37e..8542aac102ac 100644 --- a/src/lib/core/overlay/scroll/scroll-dispatcher.ts +++ b/src/lib/core/overlay/scroll/scroll-dispatcher.ts @@ -4,6 +4,7 @@ import {Subject} from 'rxjs/Subject'; import {Observable} from 'rxjs/Observable'; import {Subscription} from 'rxjs/Subscription'; import 'rxjs/add/observable/fromEvent'; +import 'rxjs/add/observable/merge'; import 'rxjs/add/operator/auditTime'; @@ -19,18 +20,18 @@ export class ScrollDispatcher { /** Subject for notifying that a registered scrollable reference element has been scrolled. */ _scrolled: Subject = new Subject(); + /** Keeps track of the global `scroll` and `resize` subscriptions. */ + _globalSubscription: Subscription = null; + + /** Keeps track of the amount of subscriptions to `scrolled`. Used for cleaning up afterwards. */ + private _scrolledCount = 0; + /** * Map of all the scrollable references that are registered with the service and their * scroll event subscriptions. */ scrollableReferences: Map = new Map(); - constructor() { - // By default, notify a scroll event when the document is scrolled or the window is resized. - Observable.fromEvent(window.document, 'scroll').subscribe(() => this._notify()); - Observable.fromEvent(window, 'resize').subscribe(() => this._notify()); - } - /** * Registers a Scrollable with the service and listens for its scrolled events. When the * scrollable is scrolled, the service emits the event in its scrolled observable. @@ -38,6 +39,7 @@ export class ScrollDispatcher { */ register(scrollable: Scrollable): void { const scrollSubscription = scrollable.elementScrolled().subscribe(() => this._notify()); + this.scrollableReferences.set(scrollable, scrollSubscription); } @@ -53,18 +55,36 @@ export class ScrollDispatcher { } /** - * Returns an observable that emits an event whenever any of the registered Scrollable + * Subscribes to an observable that emits an event whenever any of the registered Scrollable * references (or window, document, or body) fire a scrolled event. Can provide a time in ms * to override the default "throttle" time. */ - scrolled(auditTimeInMs: number = DEFAULT_SCROLL_TIME): Observable { - // In the case of a 0ms delay, return the observable without auditTime since it does add - // a perceptible delay in processing overhead. - if (auditTimeInMs == 0) { - return this._scrolled.asObservable(); + scrolled(auditTimeInMs: number = DEFAULT_SCROLL_TIME, callback: () => any): Subscription { + // In the case of a 0ms delay, use an observable without auditTime + // since it does add a perceptible delay in processing overhead. + let observable = auditTimeInMs > 0 ? + this._scrolled.asObservable().auditTime(auditTimeInMs) : + this._scrolled.asObservable(); + + this._scrolledCount++; + + if (!this._globalSubscription) { + this._globalSubscription = Observable.merge( + Observable.fromEvent(window.document, 'scroll'), + Observable.fromEvent(window, 'resize') + ).subscribe(() => this._notify()); } - return this._scrolled.asObservable().auditTime(auditTimeInMs); + // Note that we need to do the subscribing from here, in order to be able to remove + // the global event listeners once there are no more subscriptions. + return observable.subscribe(callback).add(() => { + this._scrolledCount--; + + if (this._globalSubscription && !this.scrollableReferences.size && !this._scrolledCount) { + this._globalSubscription.unsubscribe(); + this._globalSubscription = null; + } + }); } /** Returns all registered Scrollables that contain the provided element. */ diff --git a/src/lib/tooltip/tooltip.ts b/src/lib/tooltip/tooltip.ts index fc621826beaf..e146c4540655 100644 --- a/src/lib/tooltip/tooltip.ts +++ b/src/lib/tooltip/tooltip.ts @@ -150,7 +150,7 @@ export class MdTooltip implements OnInit, OnDestroy { ngOnInit() { // When a scroll on the page occurs, update the position in case this tooltip needs // to be repositioned. - this.scrollSubscription = this._scrollDispatcher.scrolled(SCROLL_THROTTLE_MS).subscribe(() => { + this.scrollSubscription = this._scrollDispatcher.scrolled(SCROLL_THROTTLE_MS, () => { if (this._overlayRef) { this._overlayRef.updatePosition(); }