Skip to content

Commit

Permalink
Support nested and window virtual scroll (angular#13862)
Browse files Browse the repository at this point in the history
[POC/WIP] This commit partially implements virtual scrolling within
closest relative parent or using window.
This is indeed not ready for merging but it demonstrates a way to handle
these use-cases with dependency injection and an abstraction of viewport
container.
  • Loading branch information
Raphael Medaer committed Jan 9, 2019
1 parent 22b0ad6 commit 1d024d6
Show file tree
Hide file tree
Showing 9 changed files with 262 additions and 50 deletions.
1 change: 1 addition & 0 deletions src/cdk/scrolling/public-api.ts
Expand Up @@ -14,3 +14,4 @@ export * from './viewport-ruler';
export * from './virtual-for-of';
export * from './virtual-scroll-strategy';
export * from './virtual-scroll-viewport';
export * from './virtual-scroll-container';
11 changes: 11 additions & 0 deletions src/cdk/scrolling/scrolling-module.ts
Expand Up @@ -13,6 +13,11 @@ import {CdkFixedSizeVirtualScroll} from './fixed-size-virtual-scroll';
import {CdkScrollable} from './scrollable';
import {CdkVirtualForOf} from './virtual-for-of';
import {CdkVirtualScrollViewport} from './virtual-scroll-viewport';
import {
CdkVirtualScrollDefaultViewport,
CdkVirtualScrollNestedViewport,
CdkVirtualScrollWindowViewport,
} from './virtual-scroll-container';

@NgModule({
imports: [BidiModule, PlatformModule],
Expand All @@ -22,12 +27,18 @@ import {CdkVirtualScrollViewport} from './virtual-scroll-viewport';
CdkScrollable,
CdkVirtualForOf,
CdkVirtualScrollViewport,
CdkVirtualScrollDefaultViewport,
CdkVirtualScrollNestedViewport,
CdkVirtualScrollWindowViewport,
],
declarations: [
CdkFixedSizeVirtualScroll,
CdkScrollable,
CdkVirtualForOf,
CdkVirtualScrollViewport,
CdkVirtualScrollDefaultViewport,
CdkVirtualScrollNestedViewport,
CdkVirtualScrollWindowViewport,
],
})
export class ScrollingModule {}
Expand Down
164 changes: 164 additions & 0 deletions src/cdk/scrolling/virtual-scroll-container.ts
@@ -0,0 +1,164 @@
import {
Directive,
ElementRef,
InjectionToken,
NgZone,
Optional,
OnDestroy,
} from '@angular/core';
import {Directionality} from '@angular/cdk/bidi';
import {ScrollDispatcher} from './scroll-dispatcher';
import {CdkScrollable, ExtendedScrollToOptions} from './scrollable';
import {fromEvent, Observable, Subject, Observer} from 'rxjs';
import {takeUntil} from 'rxjs/operators';

export const VIRTUAL_SCROLL_CONTAINER_REF =
new InjectionToken<VirtualScrollContainerRef>('VIRTUAL_SCROLL_CONTAINER_REF');

export interface VirtualScrollContainerRef {
measureScrollOffset(from: 'top' | 'left' | 'right' | 'bottom' | 'start' | 'end'): number;
measureContainerSize(orientation: 'horizontal' | 'vertical'): number;
getViewportSize(contentSize: number): string | null;
scrollTo(options: ExtendedScrollToOptions): any;
elementScrolled(): Observable<Event>;
}

@Directive({
selector: 'cdk-virtual-scroll-viewport[default]',
providers: [{
provide: VIRTUAL_SCROLL_CONTAINER_REF,
useExisting: CdkVirtualScrollDefaultViewport,
}],
host: {
'class': 'cdk-virtual-scroll-default-viewport',
},
})
export class CdkVirtualScrollDefaultViewport extends CdkScrollable implements VirtualScrollContainerRef {
measureContainerSize(orientation: 'horizontal' | 'vertical'): number {
const viewportEl = this.elementRef.nativeElement;
return orientation === 'horizontal' ? viewportEl.clientWidth : viewportEl.clientHeight;
}

getViewportSize(_: number): string | null{
return null;
}
}

export class RelativeParentElementRef implements ElementRef<any> {
constructor(private _nativeElement: any) {}

set nativeElement(_: any) {
// ignore
}

get nativeElement(): any {
// NOTE(rme): Without casting to any the following error is reached by tsc
//
// error TS2339: Property 'offsetParent' does not exist on type 'HTMLElement'.
//
return (<any>this._nativeElement).offsetParent;
}
}

@Directive({
selector: 'cdk-virtual-scroll-viewport[nested]',
providers: [{
provide: VIRTUAL_SCROLL_CONTAINER_REF,
useExisting: CdkVirtualScrollNestedViewport,
}],
host: {
'class': 'cdk-virtual-scroll-nested-viewport',
},
})
export class CdkVirtualScrollNestedViewport extends CdkScrollable implements VirtualScrollContainerRef {
constructor(
private viewportRef: ElementRef,
scrollDispatcher: ScrollDispatcher,
ngZone: NgZone,
@Optional() dir: Directionality,
) {
super(new RelativeParentElementRef(viewportRef.nativeElement), scrollDispatcher, ngZone, dir);
}

measureScrollOffset(from: 'top' | 'left' | 'right' | 'bottom' | 'start' | 'end'): number {
const offset = super.measureScrollOffset(from);

if (from == 'top') {
return offset - this.viewportRef.nativeElement.offsetTop;
}

// TODO from == left, right, bottom, start and end

return offset;
}

measureContainerSize(orientation: 'horizontal' | 'vertical'): number {
return orientation === 'horizontal' ? this.getParentNativeElement().clientWidth : this.getParentNativeElement().clientHeight;
}

getViewportSize(contentSize: number): string | null {
return contentSize + 'px';
}

private getParentNativeElement() {
return this.viewportRef.nativeElement.offsetParent;
}
}

@Directive({
selector: 'cdk-virtual-scroll-viewport[window]',
providers: [{
provide: VIRTUAL_SCROLL_CONTAINER_REF,
useExisting: CdkVirtualScrollWindowViewport,
}],
host: {
'class': 'cdk-virtual-scroll-window-viewport',
},
})
export class CdkVirtualScrollWindowViewport implements VirtualScrollContainerRef, OnDestroy {
private _destroyed = new Subject();

private _elementScrolled: Observable<Event> = Observable.create((observer: Observer<Event>) =>
this.ngZone.runOutsideAngular(() =>
fromEvent(window, 'scroll').pipe(takeUntil(this._destroyed))
.subscribe(observer)));

constructor(protected elementRef: ElementRef,
protected ngZone: NgZone,
@Optional() protected dir?: Directionality) {}

ngOnDestroy() {
this._destroyed.next();
this._destroyed.complete();
}

measureScrollOffset(from: 'top' | 'left' | 'right' | 'bottom' | 'start' | 'end'): number {
if (from == 'top') {
return -this.elementRef.nativeElement.getBoundingClientRect().top;
}

// TODO(rme) from == left, right, bottom, start and end

return 0;
}

measureContainerSize(orientation: 'horizontal' | 'vertical'): number {
return orientation === 'horizontal' ? window.innerWidth : window.innerHeight;
}

getViewportSize(contentSize: number): string | null{
return contentSize + 'px';
}

/** Returns observable that emits when a scroll event is fired on the host element. */
elementScrolled(): Observable<Event> {
return this._elementScrolled;
}

/**
* TODO(rme) doc + implement the following with dir (bottom, left, right) and rtl
*/
scrollTo(options: ExtendedScrollToOptions): void {
window.scrollTo(options);
}
}
4 changes: 3 additions & 1 deletion src/cdk/scrolling/virtual-scroll-viewport.html
Expand Up @@ -9,4 +9,6 @@
Spacer used to force the scrolling container to the correct size for the *total* number of items
so that the scrollbar captures the size of the entire data set.
-->
<div class="cdk-virtual-scroll-spacer" [style.transform]="_totalContentSizeTransform"></div>
<div class="cdk-virtual-scroll-spacer"
[style.height.px]="_totalContentHeight"
[style.width.px]="_totalContentWidth"></div>
41 changes: 14 additions & 27 deletions src/cdk/scrolling/virtual-scroll-viewport.scss
Expand Up @@ -26,9 +26,8 @@
}
}


// Scrolling container.
cdk-virtual-scroll-viewport {
.cdk-virtual-scroll-viewport {
display: block;
position: relative;
overflow: auto;
Expand All @@ -38,29 +37,32 @@ cdk-virtual-scroll-viewport {
-webkit-overflow-scrolling: touch;
}

.cdk-virtual-scroll-nested-viewport, .cdk-virtual-scroll-window-viewport {
display: block;
position: relative;
overflow: visible;
contain: none;
}

// Wrapper element for the rendered content. This element will be transformed to push the rendered
// content to its correct offset in the data set as a whole.
.cdk-virtual-scroll-content-wrapper {
position: absolute;
top: 0;
left: 0;
contain: content;

// Note: We can't put `will-change: transform;` here because it causes Safari to not update the
// viewport's `scrollHeight` when the spacer's transform changes.

[dir='rtl'] & {
right: 0;
left: auto;
}
}

.cdk-virtual-scroll-orientation-horizontal .cdk-virtual-scroll-content-wrapper {
position: absolute;
top: 0;
left: 0;
min-height: 100%;
@include _cdk-virtual-scroll-clear-container-space(horizontal);
}

.cdk-virtual-scroll-orientation-vertical .cdk-virtual-scroll-content-wrapper {
position: absolute;
top: 0;
left: 0;
min-width: 100%;
@include _cdk-virtual-scroll-clear-container-space(vertical);
}
Expand All @@ -69,19 +71,4 @@ cdk-virtual-scroll-viewport {
// set if it were rendered all at once. This ensures that the scrollable content region is the
// correct size.
.cdk-virtual-scroll-spacer {
position: absolute;
top: 0;
left: 0;
height: 1px;
width: 1px;
transform-origin: 0 0;

// Note: We can't put `will-change: transform;` here because it causes Safari to not update the
// viewport's `scrollHeight` when the spacer's transform changes.

[dir='rtl'] & {
right: 0;
left: auto;
transform-origin: 100% 0;
}
}

0 comments on commit 1d024d6

Please sign in to comment.