Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: fixed size table virtual scroll #123

Merged
merged 1 commit into from
Jun 28, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 107 additions & 0 deletions src/scrolling/fixed-size-table-virtual-scroll-strategy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { ListRange } from '@angular/cdk/collections';
import {
CdkVirtualScrollViewport,
VirtualScrollStrategy,
} from '@angular/cdk/scrolling';
import { Injectable } from '@angular/core';
import { BehaviorSubject, Subject } from 'rxjs';
import { distinctUntilChanged } from 'rxjs/operators';

@Injectable()
export class FixedSizeTableVirtualScrollStrategy
implements VirtualScrollStrategy {
private _rowHeight = 42;
private _headerHeight = 42;
private _bufferSize = 10;
private readonly _indexChange = new Subject<number>();
viewport: CdkVirtualScrollViewport;

scrolledIndexChange = this._indexChange.pipe(distinctUntilChanged());
stickyChange = new Subject<number>();

renderedRangeStream = new BehaviorSubject<ListRange>({ start: 0, end: 0 });

get dataLength(): number {
return this._dataLength;
}

set dataLength(value: number) {
if (value !== this._dataLength) {
this._dataLength = value;
this.onDataLengthChanged();
}
}

private _dataLength = 0;

attach(viewport: CdkVirtualScrollViewport): void {
this.viewport = viewport;
this.viewport.renderedRangeStream.subscribe(this.renderedRangeStream);
this.onDataLengthChanged();
}

detach(): void {
this._indexChange.complete();
this.renderedRangeStream.complete();
this.stickyChange.complete();
}

onContentScrolled(): void {
this._updateContent();
}

onDataLengthChanged(): void {
if (this.viewport) {
this.viewport.setTotalContentSize(this._rowHeight * this.dataLength);
}
this._updateContent();
}

onContentRendered(): void {
// no-op
}

onRenderedOffsetChanged(): void {
// no-op
}

scrollToIndex(index: number, behavior: ScrollBehavior): void {
if (!this.viewport || !this._rowHeight) {
return;
}
this.viewport.scrollToOffset(
(index - 1) * this._rowHeight + this._headerHeight,
behavior,
);
}

setConfig(rowHeight: number, headerHeight: number, bufferSize: number) {
this._rowHeight = rowHeight;
this._headerHeight = headerHeight;
this._bufferSize = bufferSize;
this.onDataLengthChanged();
}

private _updateContent() {
if (!this.viewport) {
return;
}
const newIndex =
Math.round(
(this.viewport.measureScrollOffset() - this._headerHeight) /
this._rowHeight,
) + 1;
const start = Math.max(0, newIndex - this._bufferSize);
const end = Math.min(
this._dataLength,
newIndex +
Math.ceil(this.viewport.getViewportSize() / this._rowHeight) +
this._bufferSize,
);
const renderedOffset = start * this._rowHeight;
this.viewport.setRenderedContentOffset(renderedOffset);
this.viewport.setRenderedRange({ start, end });
this.stickyChange.next(renderedOffset);
this._indexChange.next(newIndex);
}
}
149 changes: 149 additions & 0 deletions src/scrolling/fixed-size-table-virtual-scroll.directive.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import { VIRTUAL_SCROLL_STRATEGY } from '@angular/cdk/scrolling';
import { CdkHeaderRowDef } from '@angular/cdk/table';
import {
AfterContentInit,
ContentChild,
Directive,
Input,
OnChanges,
OnDestroy,
forwardRef,
SimpleChanges,
} from '@angular/core';
import { Subject } from 'rxjs';
import { filter, map, takeUntil, tap } from 'rxjs/operators';

import { TableComponent } from '../table/public-api';

import { FixedSizeTableVirtualScrollStrategy } from './fixed-size-table-virtual-scroll-strategy';

export function _tableVirtualScrollDirectiveStrategyFactory(
tableDir: FixedSizeTableVirtualScrollDirective,
) {
return tableDir.scrollStrategy;
}

const stickyHeaderSelector = '.aui-table__header-row.aui-table-sticky';

const defaults = {
rowHeight: 42,
headerHeight: 42,
buffer: 10,
};

@Directive({
// tslint:disable-next-line: directive-selector
selector: 'aui-virtual-scroll-viewport[fixedSize]',
exportAs: 'viewPort',
providers: [
{
provide: VIRTUAL_SCROLL_STRATEGY,
useFactory: _tableVirtualScrollDirectiveStrategyFactory,
deps: [forwardRef(() => FixedSizeTableVirtualScrollDirective)],
},
],
})
export class FixedSizeTableVirtualScrollDirective
implements AfterContentInit, OnChanges, OnDestroy {
private readonly onDestroy$ = new Subject();

@Input()
rowHeight: number = defaults.rowHeight;

@Input()
headerHeight: number = defaults.headerHeight;

@Input()
buffer: number = defaults.buffer;

@Input()
dataSource: readonly unknown[];

@ContentChild(TableComponent, { static: false })
table: TableComponent<any>;

scrollStrategy = new FixedSizeTableVirtualScrollStrategy();

private stickyPositions: Map<HTMLElement, number>;

ngAfterContentInit() {
this.scrollStrategy.stickyChange
.pipe(
filter(() => this.isStickyEnabled()),
tap(() => {
if (!this.stickyPositions) {
this.initStickyPositions();
}
}),
takeUntil(this.onDestroy$),
)
.subscribe(stickyOffset => {
this.setSticky(stickyOffset);
});
this.scrollStrategy.renderedRangeStream
.pipe(
map(({ start, end }) =>
typeof start !== 'number' || typeof end !== 'number'
? this.dataSource
: this.dataSource?.slice(start, end),
),
takeUntil(this.onDestroy$),
)
.subscribe(data => {
this.table.dataSource = data;
});
}

ngOnChanges({ dataSource, rowHeight, headerHeight, buffer }: SimpleChanges) {
if (dataSource) {
this.scrollStrategy.dataLength = this.dataSource?.length;
}
if (rowHeight || headerHeight || buffer) {
this.scrollStrategy.setConfig(
this.rowHeight,
this.headerHeight,
this.buffer,
);
}
}

ngOnDestroy() {
this.onDestroy$.next();
this.onDestroy$.complete();
}

private isStickyEnabled(): boolean {
return (
!!this.scrollStrategy.viewport &&
// @ts-expect-error
(this.table._headerRowDefs as CdkHeaderRowDef[])
.map(def => def.sticky)
.reduce((prev, curr) => prev && curr, true)
);
}

private initStickyPositions() {
this.stickyPositions = new Map<HTMLElement, number>();
this.scrollStrategy.viewport.elementRef.nativeElement
.querySelectorAll(stickyHeaderSelector)
.forEach(el => {
const parent = el.parentElement;
if (!this.stickyPositions.has(parent)) {
this.stickyPositions.set(parent, parent.offsetTop);
}
});
}

private setSticky(offset: number) {
this.scrollStrategy.viewport.elementRef.nativeElement
.querySelectorAll(stickyHeaderSelector)
.forEach((el: HTMLElement) => {
const parent = el.parentElement;
let baseOffset = 0;
if (this.stickyPositions.has(parent)) {
baseOffset = this.stickyPositions.get(parent);
}
el.style.top = `${baseOffset - offset}px`;
});
}
}
2 changes: 2 additions & 0 deletions src/scrolling/public-api.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export * from './fixed-size-table-virtual-scroll-strategy';
export * from './fixed-size-table-virtual-scroll.directive';
export * from './fixed-size-virtual-scroll.directive';
export * from './scrolling.module';
export * from './virtual-scroll-viewport.component';
2 changes: 2 additions & 0 deletions src/scrolling/scrolling.module.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { NgModule } from '@angular/core';

import { FixedSizeTableVirtualScrollDirective } from './fixed-size-table-virtual-scroll.directive';
import { FixedSizeVirtualScrollDirective } from './fixed-size-virtual-scroll.directive';
import { VirtualForOfDirective } from './virtual-for-of.directive';
import { VirtualScrollViewportComponent } from './virtual-scroll-viewport.component';

const EXPORTABLE_COMPONENTS = [VirtualScrollViewportComponent];
const EXPORTABLE_DIRECTIVES = [
FixedSizeTableVirtualScrollDirective,
FixedSizeVirtualScrollDirective,
VirtualForOfDirective,
];
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<aui-virtual-scroll-viewport
fixedSize
style="height: 400px"
[dataSource]="dataSource$$ | async"
>
<aui-table>
<ng-container auiTableColumnDef="id">
<aui-table-header-cell *auiTableHeaderCellDef>No.</aui-table-header-cell>
<aui-table-cell *auiTableCellDef="let item">
{{ item.id }}
</aui-table-cell>
</ng-container>
<ng-container auiTableColumnDef="name">
<aui-table-header-cell *auiTableHeaderCellDef>Name</aui-table-header-cell>
<aui-table-cell *auiTableCellDef="let item">
element{{ item.id }}
</aui-table-cell>
</ng-container>
<ng-container auiTableColumnDef="value">
<aui-table-header-cell *auiTableHeaderCellDef>
Value
</aui-table-header-cell>
<aui-table-cell *auiTableCellDef="let item"> Value </aui-table-cell>
</ng-container>
<ng-container auiTableColumnDef="detail">
<aui-table-header-cell *auiTableHeaderCellDef>
Detail
</aui-table-header-cell>
<aui-table-cell *auiTableCellDef="let item"> Detail info </aui-table-cell>
</ng-container>
<aui-table-header-row
*auiTableHeaderRowDef="['id', 'name', 'value', 'detail']; sticky: true"
></aui-table-header-row>
<aui-table-row
*auiTableRowDef="let row; columns: ['id', 'name', 'value', 'detail']"
></aui-table-row>
</aui-table>
</aui-virtual-scroll-viewport>
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { BehaviorSubject } from 'rxjs';

@Component({
templateUrl: 'fixed-size-virtual-scroll-demo.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class FixedSizeVirtualScrollDemoComponent {
dataSource$$ = new BehaviorSubject(
Array.from({ length: 50000 }).map((_, i) => ({ id: i + 1 })),
);
}
15 changes: 14 additions & 1 deletion stories/table/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import { IconModule, SortModule, TableModule } from '@alauda/ui';
import {
IconModule,
ScrollingModule,
SortModule,
TableModule,
} from '@alauda/ui';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { boolean, object, withKnobs } from '@storybook/addon-knobs';
import { storiesOf } from '@storybook/angular';

import { ExpandDemoComponent } from './expand-demo/expand-demo.component';
import { FixedSizeVirtualScrollDemoComponent } from './fixed-size-virtual-scroll-demo/fixed-size-virtual-scroll-demo.component';
import { SortDemoComponent } from './sort-demo/sort-demo.component';
import { StickyColumnsDemoComponent } from './sticky-columns/sticky-columns-demo.component';

Expand Down Expand Up @@ -88,6 +94,13 @@ storiesOf('Table', module)
declarations: [StickyColumnsDemoComponent],
},
component: StickyColumnsDemoComponent,
}))
.add('virtual scroll', () => ({
moduleMetadata: {
imports: [TableModule, ScrollingModule],
declarations: [FixedSizeVirtualScrollDemoComponent],
},
component: FixedSizeVirtualScrollDemoComponent,
}));

export interface Element {
Expand Down