diff --git a/src/cdk-experimental/scrolling/auto-size-virtual-scroll.ts b/src/cdk-experimental/scrolling/auto-size-virtual-scroll.ts index 92cc65497b15..b42c8062dae5 100644 --- a/src/cdk-experimental/scrolling/auto-size-virtual-scroll.ts +++ b/src/cdk-experimental/scrolling/auto-size-virtual-scroll.ts @@ -9,6 +9,7 @@ import {coerceNumberProperty} from '@angular/cdk/coercion'; import {ListRange} from '@angular/cdk/collections'; import {Directive, forwardRef, Input, OnChanges} from '@angular/core'; +import {Observable} from 'rxjs'; import {VIRTUAL_SCROLL_STRATEGY, VirtualScrollStrategy} from './virtual-scroll-strategy'; import {CdkVirtualScrollViewport} from './virtual-scroll-viewport'; @@ -65,6 +66,13 @@ export class ItemSizeAverager { /** Virtual scrolling strategy for lists with items of unknown or dynamic size. */ export class AutoSizeVirtualScrollStrategy implements VirtualScrollStrategy { + /** @docs-private Implemented as part of VirtualScrollStrategy. */ + scrolledIndexChange = Observable.create(() => { + // TODO(mmalerba): Implement. + throw Error('cdk-virtual-scroll: scrolledIndexChange is currently not supported for the' + + ' autosize scroll strategy'); + }); + /** The attached viewport. */ private _viewport: CdkVirtualScrollViewport | null = null; @@ -154,7 +162,7 @@ export class AutoSizeVirtualScrollStrategy implements VirtualScrollStrategy { /** Scroll to the offset for the given index. */ scrollToIndex(): void { // TODO(mmalerba): Implement. - throw new Error('cdk-virtual-scroll: scrollToIndex is currently not supported for the autosize' + throw Error('cdk-virtual-scroll: scrollToIndex is currently not supported for the autosize' + ' scroll strategy'); } diff --git a/src/cdk-experimental/scrolling/fixed-size-virtual-scroll.ts b/src/cdk-experimental/scrolling/fixed-size-virtual-scroll.ts index 324bc18f030d..ba9d058b06bc 100644 --- a/src/cdk-experimental/scrolling/fixed-size-virtual-scroll.ts +++ b/src/cdk-experimental/scrolling/fixed-size-virtual-scroll.ts @@ -9,12 +9,19 @@ import {coerceNumberProperty} from '@angular/cdk/coercion'; import {ListRange} from '@angular/cdk/collections'; import {Directive, forwardRef, Input, OnChanges} from '@angular/core'; +import {Observable, Subject} from 'rxjs'; +import {distinctUntilChanged} from 'rxjs/operators'; import {VIRTUAL_SCROLL_STRATEGY, VirtualScrollStrategy} from './virtual-scroll-strategy'; import {CdkVirtualScrollViewport} from './virtual-scroll-viewport'; /** Virtual scrolling strategy for lists with items of known fixed size. */ export class FixedSizeVirtualScrollStrategy implements VirtualScrollStrategy { + private _scrolledIndexChange = new Subject(); + + /** @docs-private Implemented as part of VirtualScrollStrategy. */ + scrolledIndexChange: Observable = this._scrolledIndexChange.pipe(distinctUntilChanged()); + /** The attached viewport. */ private _viewport: CdkVirtualScrollViewport | null = null; @@ -45,6 +52,7 @@ export class FixedSizeVirtualScrollStrategy implements VirtualScrollStrategy { /** Detaches this scroll strategy from the currently attached viewport. */ detach() { + this._scrolledIndexChange.complete(); this._viewport = null; } @@ -113,6 +121,8 @@ export class FixedSizeVirtualScrollStrategy implements VirtualScrollStrategy { this._bufferSize); this._viewport.setRenderedRange(range); this._viewport.setRenderedContentOffset(this._itemSize * range.start); + + this._scrolledIndexChange.next(firstVisibleIndex); } /** diff --git a/src/cdk-experimental/scrolling/virtual-scroll-strategy.ts b/src/cdk-experimental/scrolling/virtual-scroll-strategy.ts index 033f54e1c355..f5bee1630c76 100644 --- a/src/cdk-experimental/scrolling/virtual-scroll-strategy.ts +++ b/src/cdk-experimental/scrolling/virtual-scroll-strategy.ts @@ -6,8 +6,9 @@ * found in the LICENSE file at https://angular.io/license */ -import {CdkVirtualScrollViewport} from './virtual-scroll-viewport'; import {InjectionToken} from '@angular/core'; +import {Observable} from 'rxjs'; +import {CdkVirtualScrollViewport} from './virtual-scroll-viewport'; /** The injection token used to specify the virtual scrolling strategy. */ @@ -17,6 +18,9 @@ export const VIRTUAL_SCROLL_STRATEGY = /** A strategy that dictates which items should be rendered in the viewport. */ export interface VirtualScrollStrategy { + /** Emits when the index of the first element visible in the viewport changes. */ + scrolledIndexChange: Observable; + /** * Attaches this scroll strategy to a viewport. * @param viewport The viewport to attach this strategy to. diff --git a/src/cdk-experimental/scrolling/virtual-scroll-viewport.spec.ts b/src/cdk-experimental/scrolling/virtual-scroll-viewport.spec.ts index 5614b8a30a97..eb0454df5cd3 100644 --- a/src/cdk-experimental/scrolling/virtual-scroll-viewport.spec.ts +++ b/src/cdk-experimental/scrolling/virtual-scroll-viewport.spec.ts @@ -206,6 +206,21 @@ describe('CdkVirtualScrollViewport', () => { expect(viewport.getRenderedRange()).toEqual({start: 2, end: 6}); })); + it('should output scrolled index', fakeAsync(() => { + finishInit(fixture); + triggerScroll(viewport, testComponent.itemSize * 2 - 1); + fixture.detectChanges(); + flush(); + + expect(testComponent.scrolledToIndex).toBe(1); + + triggerScroll(viewport, testComponent.itemSize * 2); + fixture.detectChanges(); + flush(); + + expect(testComponent.scrolledToIndex).toBe(2); + })); + it('should update viewport as user scrolls down', fakeAsync(() => { finishInit(fixture); @@ -608,7 +623,8 @@ function triggerScroll(viewport: CdkVirtualScrollViewport, offset?: number) { template: ` + [style.height.px]="viewportHeight" [style.width.px]="viewportWidth" + (scrolledIndexChange)="scrolledToIndex = $event">
(); + /** Emits when a change detection cycle completes. */ + private _changeDetectionComplete = new Subject(); + /** The direction the viewport scrolls. */ @Input() orientation: 'horizontal' | 'vertical' = 'vertical'; + // Note: we don't use the typical EventEmitter here because we need to subscribe to the scroll + // strategy lazily (i.e. only if the user is actually listening to the events). We do this because + // depending on how the strategy calculates the scrolled index, it may come at a cost to + // performance. + /** Emits when the index of the first element visible in the viewport changes. */ + @Output() scrolledIndexChange: Observable = + Observable.create(observer => this._scrollStrategy.scrolledIndexChange + .pipe(sample(this._changeDetectionComplete)) + .subscribe(observer)); + /** The element that wraps the rendered content. */ @ViewChild('contentWrapper') _contentWrapper: ElementRef; @@ -139,6 +153,7 @@ export class CdkVirtualScrollViewport implements OnInit, OnDestroy { // Complete all subjects this._renderedRangeSubject.complete(); this._detachedSubject.complete(); + this._changeDetectionComplete.complete(); this._destroyed.complete(); } @@ -363,5 +378,7 @@ export class CdkVirtualScrollViewport implements OnInit, OnDestroy { for (const fn of runAfterChangeDetection) { fn(); } + + this._ngZone.run(() => this._changeDetectionComplete.next()); } } diff --git a/src/demo-app/virtual-scroll/virtual-scroll-demo.html b/src/demo-app/virtual-scroll/virtual-scroll-demo.html index 30b12deb7f8a..0ce50761f34e 100644 --- a/src/demo-app/virtual-scroll/virtual-scroll-demo.html +++ b/src/demo-app/virtual-scroll/virtual-scroll-demo.html @@ -56,8 +56,12 @@

Fixed size

+

+ Currently scrolled to item: {{scrolledIndex}} +

- +
Item #{{i}} - ({{size}}px) diff --git a/src/demo-app/virtual-scroll/virtual-scroll-demo.ts b/src/demo-app/virtual-scroll/virtual-scroll-demo.ts index 0f35340ff1c4..7199a6e60202 100644 --- a/src/demo-app/virtual-scroll/virtual-scroll-demo.ts +++ b/src/demo-app/virtual-scroll/virtual-scroll-demo.ts @@ -28,6 +28,7 @@ export class VirtualScrollDemo { scrollToOffset = 0; scrollToIndex = 0; scrollToBehavior: ScrollBehavior = 'auto'; + scrolledIndex = 0; fixedSizeData = Array(10000).fill(50); increasingSizeData = Array(10000).fill(0).map((_, i) => (1 + Math.floor(i / 1000)) * 20); decreasingSizeData = Array(10000).fill(0) @@ -110,4 +111,8 @@ export class VirtualScrollDemo { return 0; })); } + + scrolled(index: number) { + this.scrolledIndex = index; + } }