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.
This commit is contained in:
mmalerba
2018-07-26 10:18:45 -07:00
committed by GitHub
parent c2190e409a
commit e462f3d4bd
7 changed files with 71 additions and 5 deletions
@@ -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');
}
@@ -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<number>();
/** @docs-private Implemented as part of VirtualScrollStrategy. */
scrolledIndexChange: Observable<number> = 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);
}
/**
@@ -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<number>;
/**
* Attaches this scroll strategy to a viewport.
* @param viewport The viewport to attach this strategy to.
@@ -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: `
<cdk-virtual-scroll-viewport
[itemSize]="itemSize" [bufferSize]="bufferSize" [orientation]="orientation"
[style.height.px]="viewportHeight" [style.width.px]="viewportWidth">
[style.height.px]="viewportHeight" [style.width.px]="viewportWidth"
(scrolledIndexChange)="scrolledToIndex = $event">
<div class="item"
*cdkVirtualFor="let item of items; let i = index; trackBy: trackBy; \
templateCacheSize: templateCacheSize"
@@ -642,6 +658,8 @@ class FixedSizeVirtualScroll {
@Input() trackBy;
@Input() templateCacheSize = 20;
scrolledToIndex = 0;
get viewportWidth() {
return this.orientation == 'horizontal' ? this.viewportSize : this.viewportCrossSize;
}
@@ -18,11 +18,12 @@ import {
NgZone,
OnDestroy,
OnInit,
Output,
ViewChild,
ViewEncapsulation,
} from '@angular/core';
import {animationFrameScheduler, fromEvent, Observable, Subject} from 'rxjs';
import {sampleTime, takeUntil} from 'rxjs/operators';
import {sample, sampleTime, takeUntil} from 'rxjs/operators';
import {CdkVirtualForOf} from './virtual-for-of';
import {VIRTUAL_SCROLL_STRATEGY, VirtualScrollStrategy} from './virtual-scroll-strategy';
@@ -54,9 +55,22 @@ export class CdkVirtualScrollViewport implements OnInit, OnDestroy {
/** Emits when the rendered range changes. */
private _renderedRangeSubject = new Subject<ListRange>();
/** Emits when a change detection cycle completes. */
private _changeDetectionComplete = new Subject<void>();
/** 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<number> =
Observable.create(observer => this._scrollStrategy.scrolledIndexChange
.pipe(sample(this._changeDetectionComplete))
.subscribe(observer));
/** The element that wraps the rendered content. */
@ViewChild('contentWrapper') _contentWrapper: ElementRef<HTMLElement>;
@@ -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());
}
}
@@ -56,8 +56,12 @@
<button mat-button (click)="viewport1.scrollToIndex(scrollToIndex, scrollToBehavior)">
Go to index
</button>
<p>
Currently scrolled to item: {{scrolledIndex}}
</p>
<cdk-virtual-scroll-viewport class="demo-viewport" [itemSize]="50" #viewport1>
<cdk-virtual-scroll-viewport class="demo-viewport" [itemSize]="50" #viewport1
(scrolledIndexChange)="scrolled($event)">
<div *cdkVirtualFor="let size of fixedSizeData; let i = index" class="demo-item"
[style.height.px]="size">
Item #{{i}} - ({{size}}px)
@@ -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;
}
}