Skip to content

Commit e462f3d

Browse files
authored
virtual-scroll: emit the currently scrolled to index (#12381)
Note: `(scrolledIndexChange)` currently does not work with the `AutoSizeVirtualScrollStrategy`. Support for this will be added in a future PR.
1 parent c2190e4 commit e462f3d

7 files changed

+71
-5
lines changed

src/cdk-experimental/scrolling/auto-size-virtual-scroll.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import {coerceNumberProperty} from '@angular/cdk/coercion';
1010
import {ListRange} from '@angular/cdk/collections';
1111
import {Directive, forwardRef, Input, OnChanges} from '@angular/core';
12+
import {Observable} from 'rxjs';
1213
import {VIRTUAL_SCROLL_STRATEGY, VirtualScrollStrategy} from './virtual-scroll-strategy';
1314
import {CdkVirtualScrollViewport} from './virtual-scroll-viewport';
1415

@@ -65,6 +66,13 @@ export class ItemSizeAverager {
6566

6667
/** Virtual scrolling strategy for lists with items of unknown or dynamic size. */
6768
export class AutoSizeVirtualScrollStrategy implements VirtualScrollStrategy {
69+
/** @docs-private Implemented as part of VirtualScrollStrategy. */
70+
scrolledIndexChange = Observable.create(() => {
71+
// TODO(mmalerba): Implement.
72+
throw Error('cdk-virtual-scroll: scrolledIndexChange is currently not supported for the' +
73+
' autosize scroll strategy');
74+
});
75+
6876
/** The attached viewport. */
6977
private _viewport: CdkVirtualScrollViewport | null = null;
7078

@@ -154,7 +162,7 @@ export class AutoSizeVirtualScrollStrategy implements VirtualScrollStrategy {
154162
/** Scroll to the offset for the given index. */
155163
scrollToIndex(): void {
156164
// TODO(mmalerba): Implement.
157-
throw new Error('cdk-virtual-scroll: scrollToIndex is currently not supported for the autosize'
165+
throw Error('cdk-virtual-scroll: scrollToIndex is currently not supported for the autosize'
158166
+ ' scroll strategy');
159167
}
160168

src/cdk-experimental/scrolling/fixed-size-virtual-scroll.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,19 @@
99
import {coerceNumberProperty} from '@angular/cdk/coercion';
1010
import {ListRange} from '@angular/cdk/collections';
1111
import {Directive, forwardRef, Input, OnChanges} from '@angular/core';
12+
import {Observable, Subject} from 'rxjs';
13+
import {distinctUntilChanged} from 'rxjs/operators';
1214
import {VIRTUAL_SCROLL_STRATEGY, VirtualScrollStrategy} from './virtual-scroll-strategy';
1315
import {CdkVirtualScrollViewport} from './virtual-scroll-viewport';
1416

1517

1618
/** Virtual scrolling strategy for lists with items of known fixed size. */
1719
export class FixedSizeVirtualScrollStrategy implements VirtualScrollStrategy {
20+
private _scrolledIndexChange = new Subject<number>();
21+
22+
/** @docs-private Implemented as part of VirtualScrollStrategy. */
23+
scrolledIndexChange: Observable<number> = this._scrolledIndexChange.pipe(distinctUntilChanged());
24+
1825
/** The attached viewport. */
1926
private _viewport: CdkVirtualScrollViewport | null = null;
2027

@@ -45,6 +52,7 @@ export class FixedSizeVirtualScrollStrategy implements VirtualScrollStrategy {
4552

4653
/** Detaches this scroll strategy from the currently attached viewport. */
4754
detach() {
55+
this._scrolledIndexChange.complete();
4856
this._viewport = null;
4957
}
5058

@@ -113,6 +121,8 @@ export class FixedSizeVirtualScrollStrategy implements VirtualScrollStrategy {
113121
this._bufferSize);
114122
this._viewport.setRenderedRange(range);
115123
this._viewport.setRenderedContentOffset(this._itemSize * range.start);
124+
125+
this._scrolledIndexChange.next(firstVisibleIndex);
116126
}
117127

118128
/**

src/cdk-experimental/scrolling/virtual-scroll-strategy.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {CdkVirtualScrollViewport} from './virtual-scroll-viewport';
109
import {InjectionToken} from '@angular/core';
10+
import {Observable} from 'rxjs';
11+
import {CdkVirtualScrollViewport} from './virtual-scroll-viewport';
1112

1213

1314
/** The injection token used to specify the virtual scrolling strategy. */
@@ -17,6 +18,9 @@ export const VIRTUAL_SCROLL_STRATEGY =
1718

1819
/** A strategy that dictates which items should be rendered in the viewport. */
1920
export interface VirtualScrollStrategy {
21+
/** Emits when the index of the first element visible in the viewport changes. */
22+
scrolledIndexChange: Observable<number>;
23+
2024
/**
2125
* Attaches this scroll strategy to a viewport.
2226
* @param viewport The viewport to attach this strategy to.

src/cdk-experimental/scrolling/virtual-scroll-viewport.spec.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,21 @@ describe('CdkVirtualScrollViewport', () => {
206206
expect(viewport.getRenderedRange()).toEqual({start: 2, end: 6});
207207
}));
208208

209+
it('should output scrolled index', fakeAsync(() => {
210+
finishInit(fixture);
211+
triggerScroll(viewport, testComponent.itemSize * 2 - 1);
212+
fixture.detectChanges();
213+
flush();
214+
215+
expect(testComponent.scrolledToIndex).toBe(1);
216+
217+
triggerScroll(viewport, testComponent.itemSize * 2);
218+
fixture.detectChanges();
219+
flush();
220+
221+
expect(testComponent.scrolledToIndex).toBe(2);
222+
}));
223+
209224
it('should update viewport as user scrolls down', fakeAsync(() => {
210225
finishInit(fixture);
211226

@@ -608,7 +623,8 @@ function triggerScroll(viewport: CdkVirtualScrollViewport, offset?: number) {
608623
template: `
609624
<cdk-virtual-scroll-viewport
610625
[itemSize]="itemSize" [bufferSize]="bufferSize" [orientation]="orientation"
611-
[style.height.px]="viewportHeight" [style.width.px]="viewportWidth">
626+
[style.height.px]="viewportHeight" [style.width.px]="viewportWidth"
627+
(scrolledIndexChange)="scrolledToIndex = $event">
612628
<div class="item"
613629
*cdkVirtualFor="let item of items; let i = index; trackBy: trackBy; \
614630
templateCacheSize: templateCacheSize"
@@ -642,6 +658,8 @@ class FixedSizeVirtualScroll {
642658
@Input() trackBy;
643659
@Input() templateCacheSize = 20;
644660

661+
scrolledToIndex = 0;
662+
645663
get viewportWidth() {
646664
return this.orientation == 'horizontal' ? this.viewportSize : this.viewportCrossSize;
647665
}

src/cdk-experimental/scrolling/virtual-scroll-viewport.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,12 @@ import {
1818
NgZone,
1919
OnDestroy,
2020
OnInit,
21+
Output,
2122
ViewChild,
2223
ViewEncapsulation,
2324
} from '@angular/core';
2425
import {animationFrameScheduler, fromEvent, Observable, Subject} from 'rxjs';
25-
import {sampleTime, takeUntil} from 'rxjs/operators';
26+
import {sample, sampleTime, takeUntil} from 'rxjs/operators';
2627
import {CdkVirtualForOf} from './virtual-for-of';
2728
import {VIRTUAL_SCROLL_STRATEGY, VirtualScrollStrategy} from './virtual-scroll-strategy';
2829

@@ -54,9 +55,22 @@ export class CdkVirtualScrollViewport implements OnInit, OnDestroy {
5455
/** Emits when the rendered range changes. */
5556
private _renderedRangeSubject = new Subject<ListRange>();
5657

58+
/** Emits when a change detection cycle completes. */
59+
private _changeDetectionComplete = new Subject<void>();
60+
5761
/** The direction the viewport scrolls. */
5862
@Input() orientation: 'horizontal' | 'vertical' = 'vertical';
5963

64+
// Note: we don't use the typical EventEmitter here because we need to subscribe to the scroll
65+
// strategy lazily (i.e. only if the user is actually listening to the events). We do this because
66+
// depending on how the strategy calculates the scrolled index, it may come at a cost to
67+
// performance.
68+
/** Emits when the index of the first element visible in the viewport changes. */
69+
@Output() scrolledIndexChange: Observable<number> =
70+
Observable.create(observer => this._scrollStrategy.scrolledIndexChange
71+
.pipe(sample(this._changeDetectionComplete))
72+
.subscribe(observer));
73+
6074
/** The element that wraps the rendered content. */
6175
@ViewChild('contentWrapper') _contentWrapper: ElementRef<HTMLElement>;
6276

@@ -139,6 +153,7 @@ export class CdkVirtualScrollViewport implements OnInit, OnDestroy {
139153
// Complete all subjects
140154
this._renderedRangeSubject.complete();
141155
this._detachedSubject.complete();
156+
this._changeDetectionComplete.complete();
142157
this._destroyed.complete();
143158
}
144159

@@ -363,5 +378,7 @@ export class CdkVirtualScrollViewport implements OnInit, OnDestroy {
363378
for (const fn of runAfterChangeDetection) {
364379
fn();
365380
}
381+
382+
this._ngZone.run(() => this._changeDetectionComplete.next());
366383
}
367384
}

src/demo-app/virtual-scroll/virtual-scroll-demo.html

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,12 @@ <h2>Fixed size</h2>
5656
<button mat-button (click)="viewport1.scrollToIndex(scrollToIndex, scrollToBehavior)">
5757
Go to index
5858
</button>
59+
<p>
60+
Currently scrolled to item: {{scrolledIndex}}
61+
</p>
5962

60-
<cdk-virtual-scroll-viewport class="demo-viewport" [itemSize]="50" #viewport1>
63+
<cdk-virtual-scroll-viewport class="demo-viewport" [itemSize]="50" #viewport1
64+
(scrolledIndexChange)="scrolled($event)">
6165
<div *cdkVirtualFor="let size of fixedSizeData; let i = index" class="demo-item"
6266
[style.height.px]="size">
6367
Item #{{i}} - ({{size}}px)

src/demo-app/virtual-scroll/virtual-scroll-demo.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export class VirtualScrollDemo {
2828
scrollToOffset = 0;
2929
scrollToIndex = 0;
3030
scrollToBehavior: ScrollBehavior = 'auto';
31+
scrolledIndex = 0;
3132
fixedSizeData = Array(10000).fill(50);
3233
increasingSizeData = Array(10000).fill(0).map((_, i) => (1 + Math.floor(i / 1000)) * 20);
3334
decreasingSizeData = Array(10000).fill(0)
@@ -110,4 +111,8 @@ export class VirtualScrollDemo {
110111
return 0;
111112
}));
112113
}
114+
115+
scrolled(index: number) {
116+
this.scrolledIndex = index;
117+
}
113118
}

0 commit comments

Comments
 (0)