-
Notifications
You must be signed in to change notification settings - Fork 6.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(scroll): provide directive and service to listen to scrolling
- Loading branch information
1 parent
86123a3
commit 378281d
Showing
7 changed files
with
254 additions
and
13 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
import {inject, TestBed, async, ComponentFixture} from '@angular/core/testing'; | ||
import {NgModule, Component, ViewChild, ElementRef} from '@angular/core'; | ||
import {Scroll} from './scroll'; | ||
import {ScrollModule, Scrollable} from './scrollable'; | ||
|
||
describe('Scrollable', () => { | ||
let scroll: Scroll; | ||
let fixture: ComponentFixture<ScrollingComponent>; | ||
|
||
beforeEach(async(() => { | ||
TestBed.configureTestingModule({ | ||
imports: [ScrollModule.forRoot(), ScrollTestModule], | ||
}); | ||
|
||
TestBed.compileComponents(); | ||
})); | ||
|
||
beforeEach(inject([Scroll], (s: Scroll) => { | ||
scroll = s; | ||
|
||
fixture = TestBed.createComponent(ScrollingComponent); | ||
fixture.detectChanges(); | ||
})); | ||
|
||
it('should register the scrollable directive with the scroll service', () => { | ||
const componentScrollable = fixture.componentInstance.scrollable; | ||
expect(scroll.scrollableReferences.has(componentScrollable)).toBe(true); | ||
}); | ||
|
||
it('should deregister the scrollable directive when the component is destroyed', () => { | ||
const componentScrollable = fixture.componentInstance.scrollable; | ||
expect(scroll.scrollableReferences.has(componentScrollable)).toBe(true); | ||
|
||
fixture.destroy(); | ||
expect(scroll.scrollableReferences.has(componentScrollable)).toBe(false); | ||
}); | ||
|
||
it('should notify through the directive and service that a scroll event occurred', () => { | ||
let hasDirectiveScrollNotified = false; | ||
// Listen for notifications from scroll directive | ||
let scrollable = fixture.componentInstance.scrollable; | ||
scrollable.elementScrolled().subscribe(() => { hasDirectiveScrollNotified = true; }); | ||
|
||
// Listen for notifications from scroll service | ||
let hasServiceScrollNotified = false; | ||
scroll.scrolled().subscribe(() => { 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. | ||
// The notification should be picked up by the service. | ||
fixture.componentInstance.scrollingElement.nativeElement.dispatchEvent(new Event('scroll')); | ||
|
||
expect(hasDirectiveScrollNotified).toBe(true); | ||
expect(hasServiceScrollNotified).toBe(true); | ||
}); | ||
}); | ||
|
||
|
||
/** Simple component that contains a large div and can be scrolled. */ | ||
@Component({ | ||
template: `<div #scrollingElement md-scrollable style="height: 9999px"></div>` | ||
}) | ||
class ScrollingComponent { | ||
@ViewChild(Scrollable) scrollable: Scrollable; | ||
@ViewChild('scrollingElement') scrollingElement: ElementRef; | ||
} | ||
|
||
const TEST_COMPONENTS = [ScrollingComponent]; | ||
@NgModule({ | ||
imports: [ScrollModule], | ||
providers: [Scroll], | ||
exports: TEST_COMPONENTS, | ||
declarations: TEST_COMPONENTS, | ||
entryComponents: TEST_COMPONENTS, | ||
}) | ||
class ScrollTestModule { } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
import {Injectable} from '@angular/core'; | ||
import {Scrollable} from './scrollable'; | ||
import {Subject} from 'rxjs/Subject'; | ||
import {Observable} from 'rxjs/Observable'; | ||
import {Subscription} from 'rxjs/Subscription'; | ||
|
||
|
||
/** | ||
* Service contained all registered Scrollable references and emits an event when any one of the | ||
* Scrollable references emit a scrolled event. | ||
*/ | ||
@Injectable() | ||
export class Scroll { | ||
/** Subject for notifying that a registered scrollable reference element has been scrolled. */ | ||
_scrolled: Subject<Event> = new Subject(); | ||
|
||
/** | ||
* Map of all the scrollable references that are registered with the service and their | ||
* scroll event subscriptions. | ||
*/ | ||
scrollableReferences: Map<Scrollable, Subscription> = new Map(); | ||
|
||
constructor() { | ||
// By default, notify a scroll event when the document is scrolled or the window is resized. | ||
window.document.addEventListener('scroll', this._notify.bind(this)); | ||
window.addEventListener('resize', this._notify.bind(this)); | ||
} | ||
|
||
/** | ||
* 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. | ||
*/ | ||
register(scrollable: Scrollable): void { | ||
const scrollSubscription = scrollable.elementScrolled().subscribe(this._notify.bind(this)); | ||
this.scrollableReferences.set(scrollable, scrollSubscription); | ||
} | ||
|
||
/** | ||
* Deregisters a Scrollable reference and unsubscribes from its scroll event observable. | ||
*/ | ||
deregister(scrollable: Scrollable): void { | ||
this.scrollableReferences.get(scrollable).unsubscribe(); | ||
this.scrollableReferences.delete(scrollable); | ||
} | ||
|
||
/** | ||
* Returns an observable that emits an event whenever any of the registered Scrollable | ||
* references (or window, document, or body) fire a scrolled event. | ||
* TODO: Add an event limiter that includes throttle with the leading and trailing events. | ||
*/ | ||
scrolled(): Observable<Event> { | ||
return this._scrolled.asObservable(); | ||
} | ||
|
||
/** Sends a notification that a scroll event has been fired. */ | ||
_notify(e: Event) { | ||
this._scrolled.next(e); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
import { | ||
Directive, ElementRef, OnInit, OnDestroy, ModuleWithProviders, | ||
NgModule | ||
} from '@angular/core'; | ||
import {Subject} from 'rxjs/Subject'; | ||
import {Observable} from 'rxjs/Observable'; | ||
import {Scroll} from './scroll'; | ||
|
||
|
||
/** | ||
* Sends an event when the directive's element is scrolled. Registers itself with the Scroll | ||
* service to include itself as part of its collection of scrolling events that it can be listened | ||
* to through the service. | ||
*/ | ||
@Directive({ | ||
selector: '[md-scrollable]' | ||
}) | ||
export class Scrollable implements OnInit, OnDestroy { | ||
/** Subject for notifying that the element has been scrolled. */ | ||
private _elementScrolled: Subject<Event> = new Subject(); | ||
|
||
constructor(private _elementRef: ElementRef, private _scroll: Scroll) {} | ||
|
||
ngOnInit() { | ||
this._scroll.register(this); | ||
this._elementRef.nativeElement.addEventListener('scroll', (e: Event) => { | ||
this._elementScrolled.next(e); | ||
}); | ||
} | ||
|
||
ngOnDestroy() { | ||
this._scroll.deregister(this); | ||
} | ||
|
||
/** Returns observable that emits an event when the scroll event is fired on the host element. */ | ||
elementScrolled(): Observable<Event> { | ||
return this._elementScrolled.asObservable(); | ||
} | ||
} | ||
|
||
|
||
@NgModule({ | ||
exports: [Scrollable], | ||
declarations: [Scrollable], | ||
}) | ||
export class ScrollModule { | ||
static forRoot(): ModuleWithProviders { | ||
return { | ||
ngModule: ScrollModule, | ||
providers: [Scroll] | ||
}; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters