diff --git a/src/cdk-experimental/scrolling/virtual-scroll-viewport.spec.ts b/src/cdk-experimental/scrolling/virtual-scroll-viewport.spec.ts index 2f1363d8e018..544f0f143c27 100644 --- a/src/cdk-experimental/scrolling/virtual-scroll-viewport.spec.ts +++ b/src/cdk-experimental/scrolling/virtual-scroll-viewport.spec.ts @@ -10,17 +10,17 @@ import {CdkVirtualScrollViewport} from './virtual-scroll-viewport'; describe('CdkVirtualScrollViewport', () => { describe ('with FixedSizeVirtualScrollStrategy', () => { - let fixture: ComponentFixture; - let testComponent: FixedVirtualScroll; + let fixture: ComponentFixture; + let testComponent: FixedSizeVirtualScroll; let viewport: CdkVirtualScrollViewport; beforeEach(() => { TestBed.configureTestingModule({ imports: [ScrollingModule], - declarations: [FixedVirtualScroll], + declarations: [FixedSizeVirtualScroll], }).compileComponents(); - fixture = TestBed.createComponent(FixedVirtualScroll); + fixture = TestBed.createComponent(FixedSizeVirtualScroll); testComponent = fixture.componentInstance; viewport = testComponent.viewport; }); @@ -454,6 +454,45 @@ describe('CdkVirtualScrollViewport', () => { expect(testComponent.virtualForViewContainer.createEmbeddedView).toHaveBeenCalledTimes(5); })); }); + + describe ('with AutoSizeVirtualScrollStrategy', () => { + let fixture: ComponentFixture; + let testComponent: AutoSizeVirtualScroll; + let viewport: CdkVirtualScrollViewport; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ScrollingModule], + declarations: [AutoSizeVirtualScroll], + }).compileComponents(); + + fixture = TestBed.createComponent(AutoSizeVirtualScroll); + testComponent = fixture.componentInstance; + viewport = testComponent.viewport; + }); + + it('should render initial state for uniform items', fakeAsync(() => { + finishInit(fixture); + + const contentWrapper = + viewport.elementRef.nativeElement.querySelector('.cdk-virtual-scroll-content-wrapper'); + expect(contentWrapper.children.length) + .toBe(4, 'should render 4 50px items to fill 200px space'); + })); + + it('should render extra content if first item is smaller than average', fakeAsync(() => { + testComponent.items = [50, 200, 200, 200, 200, 200]; + finishInit(fixture); + + const contentWrapper = + viewport.elementRef.nativeElement.querySelector('.cdk-virtual-scroll-content-wrapper'); + expect(contentWrapper.children.length).toBe(4, + 'should render 4 items to fill 200px space based on 50px estimate from first item'); + })); + + // TODO(mmalerba): Add test that it corrects the initial render if it didn't render enough, + // once it actually does that. + }); }); @@ -506,7 +545,7 @@ function triggerScroll(viewport: CdkVirtualScrollViewport, offset?: number) { `], encapsulation: ViewEncapsulation.None, }) -class FixedVirtualScroll { +class FixedSizeVirtualScroll { @ViewChild(CdkVirtualScrollViewport) viewport: CdkVirtualScrollViewport; @ViewChild(CdkVirtualForOf, {read: ViewContainerRef}) virtualForViewContainer: ViewContainerRef; @@ -527,3 +566,46 @@ class FixedVirtualScroll { return this.orientation == 'horizontal' ? this.viewportCrossSize : this.viewportSize; } } + +@Component({ + template: ` + +
+ {{i}} - {{size}} +
+
+ `, + styles: [` + .cdk-virtual-scroll-content-wrapper { + display: flex; + flex-direction: column; + } + + .cdk-virtual-scroll-orientation-horizontal .cdk-virtual-scroll-content-wrapper { + flex-direction: row; + } + `], + encapsulation: ViewEncapsulation.None, +}) +class AutoSizeVirtualScroll { + @ViewChild(CdkVirtualScrollViewport) viewport: CdkVirtualScrollViewport; + + @Input() orientation = 'vertical'; + @Input() viewportSize = 200; + @Input() viewportCrossSize = 100; + @Input() minBufferSize = 0; + @Input() addBufferSize = 0; + @Input() items = Array(10).fill(50); + + get viewportWidth() { + return this.orientation == 'horizontal' ? this.viewportSize : this.viewportCrossSize; + } + + get viewportHeight() { + return this.orientation == 'horizontal' ? this.viewportCrossSize : this.viewportSize; + } +} diff --git a/src/cdk-experimental/scrolling/virtual-scroll-viewport.ts b/src/cdk-experimental/scrolling/virtual-scroll-viewport.ts index a0ad24085591..33c95dbc7d97 100644 --- a/src/cdk-experimental/scrolling/virtual-scroll-viewport.ts +++ b/src/cdk-experimental/scrolling/virtual-scroll-viewport.ts @@ -99,6 +99,9 @@ export class CdkVirtualScrollViewport implements DoCheck, OnInit, OnDestroy { */ private _renderedContentOffsetNeedsRewrite = false; + /** Observable that emits when the viewport is destroyed. */ + private _destroyed = new Subject(); + constructor(public elementRef: ElementRef, private _changeDetectorRef: ChangeDetectorRef, private _ngZone: NgZone, private _sanitizer: DomSanitizer, @Inject(VIRTUAL_SCROLL_STRATEGY) private _scrollStrategy: VirtualScrollStrategy) {} @@ -114,7 +117,7 @@ export class CdkVirtualScrollViewport implements DoCheck, OnInit, OnDestroy { fromEvent(viewportEl, 'scroll') // Sample the scroll stream at every animation frame. This way if there are multiple // scroll events in the same frame we only need to recheck our layout once. - .pipe(sampleTime(0, animationFrameScheduler)) + .pipe(sampleTime(0, animationFrameScheduler), takeUntil(this._destroyed)) .subscribe(() => this._scrollStrategy.onContentScrolled()); }); }); @@ -135,10 +138,12 @@ export class CdkVirtualScrollViewport implements DoCheck, OnInit, OnDestroy { ngOnDestroy() { this.detach(); this._scrollStrategy.detach(); + this._destroyed.next(); // Complete all subjects this._renderedRangeSubject.complete(); this._detachedSubject.complete(); + this._destroyed.complete(); } /** Attaches a `CdkVirtualForOf` to this viewport. */ @@ -208,20 +213,17 @@ export class CdkVirtualScrollViewport implements DoCheck, OnInit, OnDestroy { this._ngZone.run(() => { this._renderedRangeSubject.next(this._renderedRange = range); this._changeDetectorRef.markForCheck(); - this._ngZone.runOutsideAngular(() => this._ngZone.onStable.pipe(take(1)).subscribe(() => { - // Queue this up in a `Promise.resolve()` so that if the user makes a series of calls - // like: - // - // viewport.setRenderedRange(...); - // viewport.setTotalContentSize(...); - // viewport.setRenderedContentOffset(...); - // - // The call to `onContentRendered` will happen after all of the updates have been applied. - Promise.resolve().then(() => { - this._scrollStrategy.onContentRendered(); - }); - })); }); + // Queue this up in a `Promise.resolve()` so that if the user makes a series of calls + // like: + // + // viewport.setRenderedRange(...); + // viewport.setTotalContentSize(...); + // viewport.setRenderedContentOffset(...); + // + // The call to `onContentRendered` will happen after all of the updates have been applied. + this._ngZone.runOutsideAngular(() => this._ngZone.onStable.pipe(take(1)).subscribe( + () => Promise.resolve().then(() => this._scrollStrategy.onContentRendered()))); } }