Skip to content

Commit

Permalink
chore: updated scrollable
Browse files Browse the repository at this point in the history
  • Loading branch information
pimenovoleg committed Oct 22, 2018
1 parent 63eae6e commit c3cd645
Show file tree
Hide file tree
Showing 8 changed files with 290 additions and 100 deletions.
14 changes: 14 additions & 0 deletions src/cdk/scrolling/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
The `scrolling` package provides helpers for directives that react to scroll events.

### cdkScrollable and ScrollDispatcher
The `cdkScrollable` directive and the `ScrollDispatcher` service together allow components to
react to scrolling in any of its ancestor scrolling containers.

The `cdkScrollable` directive should be applied to any element that acts as a scrolling container.
This marks the element as a `Scrollable` and registers it with the `ScrollDispatcher`. The
dispatcher, then, allows components to share both event listeners and knowledge of all of the
scrollable containers in the application.

### ViewportRuler
The `ViewportRuler` is a service that can be injected and used to measure the bounds of the browser
viewport.
2 changes: 1 addition & 1 deletion src/cdk/scrolling/public-api.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@

export * from './scroll-dispatcher';
export * from './scrollable';
export * from './viewport-ruler';
export * from './scrolling-module';
export * from './viewport-ruler';
40 changes: 17 additions & 23 deletions src/cdk/scrolling/scroll-dispatcher.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { NgModule, Component, ViewChild, ElementRef } from '@angular/core';
import { inject, TestBed, async, fakeAsync, ComponentFixture, tick } from '@angular/core/testing';
import { dispatchFakeEvent } from '@ptsecurity/cdk/testing';

import { CdkScrollable, ScrollDispatcher, ScrollDispatchModule } from './public-api';
import { CdkScrollable, ScrollDispatcher, ScrollingModule } from './public-api';


describe('ScrollDispatcher', () => {
Expand Down Expand Up @@ -69,16 +69,15 @@ describe('ScrollDispatcher', () => {
}));

it('should not execute the global events in the Angular zone', () => {
scroll.scrolled(0).subscribe(() => {
}); // tslint:disable-line
scroll.scrolled(0).subscribe(() => {});
dispatchFakeEvent(document, 'scroll', false);

expect(fixture.ngZone!.isStable).toBe(true); // tslint:disable-line
expect(fixture.ngZone!.isStable).toBe(true);
});

it('should not execute the scrollable events in the Angular zone', () => {
dispatchFakeEvent(fixture.componentInstance.scrollingElement.nativeElement, 'scroll');
expect(fixture.ngZone!.isStable).toBe(true); // tslint:disable-line
expect(fixture.ngZone!.isStable).toBe(true);
});

it('should be able to unsubscribe from the global scrollable', () => {
Expand Down Expand Up @@ -114,12 +113,13 @@ describe('ScrollDispatcher', () => {
expect(spy).toHaveBeenCalled();
subscription.unsubscribe();
});

});

describe('Nested scrollables', () => {
let scroll: ScrollDispatcher;
let fixture: ComponentFixture<NestedScrollingComponent>;
let element: ElementRef;
let element: ElementRef<HTMLElement>;

beforeEach(inject([ScrollDispatcher], (s: ScrollDispatcher) => {
scroll = s;
Expand All @@ -146,7 +146,7 @@ describe('ScrollDispatcher', () => {
expect(spy).toHaveBeenCalledTimes(1);

dispatchFakeEvent(window.document, 'scroll', false);
expect(spy).toHaveBeenCalledTimes(2); // tslint:disable-line
expect(spy).toHaveBeenCalledTimes(2);

subscription.unsubscribe();
});
Expand All @@ -173,8 +173,7 @@ describe('ScrollDispatcher', () => {
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).subscribe(() => {
}); // tslint:disable-line
const subscription = scroll.scrolled(0).subscribe(() => {});

expect(scroll._globalSubscription).toBeTruthy(
'Expected global listeners after a subscription has been added.');
Expand All @@ -190,10 +189,9 @@ describe('ScrollDispatcher', () => {
fixture.detectChanges();

expect(scroll._globalSubscription).toBeNull('Expected no global listeners on init.');
expect(scroll.scrollContainers.size).toBe(4, 'Expected multiple scrollables'); // tslint:disable-line
expect(scroll.scrollContainers.size).toBe(4, 'Expected multiple scrollables');

const subscription = scroll.scrolled(0).subscribe(() => {
}); // tslint:disable-line
const subscription = scroll.scrolled(0).subscribe(() => {});

expect(scroll._globalSubscription).toBeTruthy(
'Expected global listeners after a subscription has been added.');
Expand All @@ -203,14 +201,13 @@ describe('ScrollDispatcher', () => {
expect(scroll._globalSubscription).toBeNull(
'Expected global listeners to have been removed after the subscription has stopped.');
expect(scroll.scrollContainers.size)
.toBe(4, 'Expected scrollable count to stay the same'); // tslint:disable-line
.toBe(4, 'Expected scrollable count to stay the same');
});

it('should remove the global subscription on destroy', () => {
expect(scroll._globalSubscription).toBeNull('Expected no global listeners on init.');

const subscription = scroll.scrolled(0).subscribe(() => {
}); // tslint:disable-line
const subscription = scroll.scrolled(0).subscribe(() => {});

expect(scroll._globalSubscription).toBeTruthy(
'Expected global listeners after a subscription has been added.');
Expand All @@ -229,12 +226,11 @@ describe('ScrollDispatcher', () => {

/** Simple component that contains a large div and can be scrolled. */
@Component({
template: `
<div #scrollingElement cdk-scrollable style="height: 9999px"></div>`
template: `<div #scrollingElement cdk-scrollable style="height: 9999px"></div>`
})
class ScrollingComponent {
@ViewChild(CdkScrollable) scrollable: CdkScrollable;
@ViewChild('scrollingElement') scrollingElement: ElementRef;
@ViewChild('scrollingElement') scrollingElement: ElementRef<HTMLElement>;
}


Expand All @@ -251,17 +247,15 @@ class ScrollingComponent {
`
})
class NestedScrollingComponent {
@ViewChild('interestingElement') interestingElement: ElementRef;
@ViewChild('interestingElement') interestingElement: ElementRef<HTMLElement>;
}

const TEST_COMPONENTS = [ScrollingComponent, NestedScrollingComponent];

@NgModule({
imports: [ScrollDispatchModule],
imports: [ScrollingModule],
providers: [ScrollDispatcher],
exports: TEST_COMPONENTS,
declarations: TEST_COMPONENTS,
entryComponents: TEST_COMPONENTS
})
class ScrollTestModule {
}
class ScrollTestModule { }
42 changes: 23 additions & 19 deletions src/cdk/scrolling/scroll-dispatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
OnDestroy
} from '@angular/core';
import { Platform } from '@ptsecurity/cdk/platform';
import { fromEvent, of as observableOf, Subject, Subscription, Observable } from 'rxjs';
import { fromEvent, of as observableOf, Subject, Subscription, Observable, Observer } from 'rxjs';
import { auditTime, filter } from 'rxjs/operators';

import { CdkScrollable } from './scrollable';
Expand All @@ -31,12 +31,12 @@ export class ScrollDispatcher implements OnDestroy {
_globalSubscription: Subscription | null = null;

/** Subject for notifying that a registered scrollable reference element has been scrolled. */
private _scrolled = new Subject<CdkScrollable | {} | void>();
private _scrolled = new Subject<CdkScrollable | void>();

/** Keeps track of the amount of subscriptions to `scrolled`. Used for cleaning up afterwards. */
private _scrolledCount = 0;
private scrolledCount = 0;

constructor(private _ngZone: NgZone, private _platform: Platform) { }
constructor(private ngZone: NgZone, private platform: Platform) { }

/**
* Registers a scrollable instance with the service and listens for its scrolled events. When the
Expand Down Expand Up @@ -74,9 +74,14 @@ export class ScrollDispatcher implements OnDestroy {
* to run the callback using `NgZone.run`.
*/
scrolled(auditTimeInMs: number = DEFAULT_SCROLL_TIME): Observable<CdkScrollable | void> {
return this._platform.isBrowser ? Observable.create((observer) => {

if (!this.platform.isBrowser) {
return observableOf<void>();
}

return Observable.create((observer: Observer<CdkScrollable | void>) => {
if (!this._globalSubscription) {
this._addGlobalListener();
this.addGlobalListener();
}

// In the case of a 0ms delay, use an observable without auditTime
Expand All @@ -85,21 +90,21 @@ export class ScrollDispatcher implements OnDestroy {
this._scrolled.pipe(auditTime(auditTimeInMs)).subscribe(observer) :
this._scrolled.subscribe(observer);

this._scrolledCount++;
this.scrolledCount++;

return () => {
subscription.unsubscribe();
this._scrolledCount--;
this.scrolledCount--;

if (!this._scrolledCount) {
this._removeGlobalListener();
if (!this.scrolledCount) {
this.removeGlobalListener();
}
};
}) : observableOf<void>();
});
}

ngOnDestroy() {
this._removeGlobalListener();
this.removeGlobalListener();
this.scrollContainers.forEach((_, container) => this.deregister(container));
this._scrolled.complete();
}
Expand All @@ -113,8 +118,7 @@ export class ScrollDispatcher implements OnDestroy {
ancestorScrolled(elementRef: ElementRef, auditTimeInMs?: number): Observable<CdkScrollable | void> {
const ancestors = this.getAncestorScrollContainers(elementRef);

return this.scrolled(auditTimeInMs).pipe(filter((target: any) => {

return this.scrolled(auditTimeInMs).pipe(filter((target) => {
return !target || ancestors.indexOf(target) > -1;
}));
}
Expand All @@ -124,7 +128,7 @@ export class ScrollDispatcher implements OnDestroy {
const scrollingContainers: CdkScrollable[] = [];

this.scrollContainers.forEach((_subscription: Subscription, scrollable: CdkScrollable) => {
if (this._scrollableContainsElement(scrollable, elementRef)) {
if (this.scrollableContainsElement(scrollable, elementRef)) {
scrollingContainers.push(scrollable);
}
});
Expand All @@ -133,7 +137,7 @@ export class ScrollDispatcher implements OnDestroy {
}

/** Returns true if the element is contained within the provided Scrollable. */
private _scrollableContainsElement(scrollable: CdkScrollable, elementRef: ElementRef): boolean {
private scrollableContainsElement(scrollable: CdkScrollable, elementRef: ElementRef): boolean {
let element = elementRef.nativeElement;
let scrollableElement = scrollable.getElementRef().nativeElement; //tslint:disable-line

Expand All @@ -147,14 +151,14 @@ export class ScrollDispatcher implements OnDestroy {
}

/** Sets up the global scroll listeners. */
private _addGlobalListener() {
this._globalSubscription = this._ngZone.runOutsideAngular(() => {
private addGlobalListener() {
this._globalSubscription = this.ngZone.runOutsideAngular(() => {
return fromEvent(window.document, 'scroll').subscribe(() => this._scrolled.next());
});
}

/** Cleans up the global scroll listener. */
private _removeGlobalListener() {
private removeGlobalListener() {
if (this._globalSubscription) {
this._globalSubscription.unsubscribe();
this._globalSubscription = null;
Expand Down
Loading

0 comments on commit c3cd645

Please sign in to comment.