Skip to content

Commit

Permalink
feat(ngrid): cache when rendering rows
Browse files Browse the repository at this point in the history
When scrolling with virtual scroll sometimes rows are added/removed at the edges, based on the size and position. This was expansive because it created and destroyed DOM elements. Now we cache these elements and their view's and just insert them when they are ready to join the grid again.
  • Loading branch information
shlomiassaf committed Dec 3, 2020
1 parent 35bbea8 commit 170c2d4
Show file tree
Hide file tree
Showing 8 changed files with 287 additions and 104 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -225,8 +225,6 @@ export class PblNgridDynamicVirtualScrollStrategy implements PblNgridVirtualScro
let contentOffset = this.sizer.getSizeBefore(newRange.start);
const currentStartBuffer = scrollOffset - contentOffset;

console.log(`VP: ${viewportSize} | ITEMS: ${dataLength} | SCROLL: ${scrollOffset} | FirstVisible: ${firstVisibleIndex} | FirstRenderPx: ${contentOffset} | StartBuffer: ${currentStartBuffer}`);

if (currentStartBuffer < this._minBufferPx && newRange.start !== 0) {
let spaceToFill = this._maxBufferPx - currentStartBuffer;
if (spaceToFill < 0) {
Expand Down Expand Up @@ -255,7 +253,6 @@ export class PblNgridDynamicVirtualScrollStrategy implements PblNgridVirtualScro
const renderDataEnd = contentOffset + this.sizer.getSizeForRange(newRange.start, newRange.end);
const currentEndBuffer = renderDataEnd - (scrollOffset + viewportSize);
if (currentEndBuffer < this._minBufferPx && newRange.end !== dataLength) {
console.log(`EndBuff: ${currentEndBuffer}`);
let spaceToFill = this._maxBufferPx - currentEndBuffer;
if (spaceToFill < 0) {
spaceToFill = Math.abs(spaceToFill) + this._maxBufferPx;
Expand All @@ -282,11 +279,7 @@ export class PblNgridDynamicVirtualScrollStrategy implements PblNgridVirtualScro
}
}

if (renderedRange.start !== newRange.start || renderedRange.end !== newRange.end) {
console.log(renderedRange, newRange, contentOffset);
}

this._lastExcessHeight = excessHeight;
this._lastExcessHeight = excessHeight;
this._viewport.setRenderedRange(newRange);
this._viewport.setRenderedContentOffset(contentOffset + excessHeight);
this._scrolledIndexChange.next(Math.floor(firstVisibleIndex));
Expand Down
43 changes: 26 additions & 17 deletions libs/ngrid/src/lib/grid/features/virtual-scroll/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,6 @@ export function updateStickyRows(offset: number, rows: HTMLElement[], stickyStat
export function measureRangeSize(viewContainer: ViewContainerRef,
range: ListRange,
renderedRange: ListRange,
orientation: 'horizontal' | 'vertical',
stickyState: boolean[] = []): number {
if (range.start >= range.end) {
return 0;
Expand All @@ -91,31 +90,41 @@ export function measureRangeSize(viewContainer: ViewContainerRef,
// The index into the list of rendered views for the first item in the range.
const renderedStartIndex = range.start - renderedRange.start;
// The length of the range we're measuring.
const rangeLen = range.end - range.start + 1;
const rangeLen = range.end - range.start;

// Loop over all root nodes for all items in the range and sum up their size.
let totalSize = 0;
let i = rangeLen;
while (i--) {
const index = i + renderedStartIndex;
if (!stickyState[index]) {
const view = viewContainer.get(index) as EmbeddedViewRef<any> | null;
let j = view ? view.rootNodes.length : 0;
while (j--) {
totalSize += getSize(orientation, view.rootNodes[j]);
}
// Loop over all the views, find the first and land node and compute the size by subtracting
// the top of the first node from the bottom of the last one.
let firstNode: HTMLElement | undefined;
let lastNode: HTMLElement | undefined;

// Find the first node by starting from the beginning and going forwards.
for (let i = 0; i < rangeLen; i++) {
const view = viewContainer.get(i + renderedStartIndex) as EmbeddedViewRef<any> | null;
if (view && view.rootNodes.length) {
firstNode = lastNode = view.rootNodes[0];
break;
}
}

return totalSize;
// Find the last node by starting from the end and going backwards.
for (let i = rangeLen - 1; i > -1; i--) {
const view = viewContainer.get(i + renderedStartIndex) as EmbeddedViewRef<any> | null;
if (view && view.rootNodes.length) {
lastNode = view.rootNodes[view.rootNodes.length - 1];
break;
}
}

return firstNode && lastNode ? getOffset('end', lastNode) - getOffset('start', firstNode) : 0;
}

/** Helper to extract size from a DOM Node. */
function getSize(orientation: 'horizontal' | 'vertical', node: Node): number {
/** Helper to extract the offset of a DOM Node in a certain direction. */
function getOffset(direction: 'start' | 'end', node: Node) {
const el = node as Element;
if (!el.getBoundingClientRect) {
return 0;
}
const rect = el.getBoundingClientRect();
return orientation === 'horizontal' ? rect.width : rect.height;

return direction === 'start' ? rect.top : rect.bottom;
}
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ export class PblVirtualScrollForOf<T> implements CollectionViewer, NgeVirtualTab

const vcRefs = [this.vcRefs.header, this.vcRefs.data, this.vcRefs.footer];
const vcRefSizeReducer = (total: number, vcRef: ViewContainerRef, index: number): number => {
return total + measureRangeSize(vcRef, ranges[index], renderedRanges[index], orientation, stickyStates[index]);
return total + measureRangeSize(vcRef, ranges[index], renderedRanges[index], stickyStates[index]);
};

return vcRefs.reduce(vcRefSizeReducer, 0);
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { EmbeddedViewRef, Inject, Injectable, IterableChangeRecord, IterableChanges, ViewContainerRef } from '@angular/core';
import {
_ViewRepeater,
_ViewRepeaterItemChange,
_ViewRepeaterItemChanged,
_ViewRepeaterItemContext,
_ViewRepeaterItemContextFactory,
_ViewRepeaterItemInsertArgs,
_ViewRepeaterItemValueResolver,
_ViewRepeaterOperation,
} from '@angular/cdk/collections';
import { CdkRowDef, RenderRow, BaseRowDef, RowContext } from '@angular/cdk/table';

import { EXT_API_TOKEN, PblNgridInternalExtensionApi } from '../../ext/grid-ext-api';
import { PblRowContext } from '../context/row';

export interface BaseChangeOperationState<T, R extends RenderRow<T>, C extends PblRowContext<T>> {
vcRef: ViewContainerRef;
itemContextFactory: _ViewRepeaterItemContextFactory<T, R, C>;
itemValueResolver: _ViewRepeaterItemValueResolver<T, R>;
}

export interface ChangeOperationState<T, R extends RenderRow<T>, C extends PblRowContext<T>> extends BaseChangeOperationState<T, R, C> {
record: IterableChangeRecord<R>;
view?: EmbeddedViewRef<C> | undefined;
op?: _ViewRepeaterOperation;
}

@Injectable()
export class PblNgridBaseRowViewRepeaterStrategy<T, R extends RenderRow<T>, C extends PblRowContext<T>> implements _ViewRepeater<T, R, C> {
protected workaroundEnabled = false;
protected renderer: { _renderCellTemplateForItem: (rowDef: BaseRowDef, context: RowContext<T>) => void; };
protected _cachedRenderDefMap = new Map<number, CdkRowDef<T>>();

constructor(@Inject(EXT_API_TOKEN) protected extApi: PblNgridInternalExtensionApi<T>) {
extApi
.onConstructed(() => {
const cdkTable = extApi.cdkTable;
this.renderer = cdkTable as any;
this.workaroundEnabled = !cdkTable['_cachedRenderDefMap'] && typeof this.renderer._renderCellTemplateForItem === 'function';
});
}

applyChanges(changes: IterableChanges<R>,
vcRef: ViewContainerRef,
itemContextFactory: _ViewRepeaterItemContextFactory<T, R, C>,
itemValueResolver: _ViewRepeaterItemValueResolver<T, R>,
itemViewChanged?: _ViewRepeaterItemChanged<R, C>) {
const baseState: BaseChangeOperationState<T, R, C> = {
vcRef,
itemContextFactory: ( record: IterableChangeRecord<R>,
adjustedPreviousIndex: number | null,
currentIndex: number | null) => this.updateWithNgridContext(itemContextFactory(record, adjustedPreviousIndex, currentIndex)),
itemValueResolver,
};
changes.forEachOperation((record: IterableChangeRecord<R>, adjustedPreviousIndex: number | null, currentIndex: number | null) => {
const state: ChangeOperationState<T, R, C> = Object.create(baseState);
state.record = record;
if (record.previousIndex == null) {
this.addItem(adjustedPreviousIndex, currentIndex, state);
} else if (currentIndex == null) {
this.removeItem(adjustedPreviousIndex, state);
} else {
this.moveItem(adjustedPreviousIndex, currentIndex, state);
}

if (this.workaroundEnabled) {
this.patch20765afterOp(state);
}

this.afterOperation(state);
});

if (this.workaroundEnabled) {
this.patch20765(vcRef, changes, itemContextFactory);
}
}

detach(): void { }

protected addItem(adjustedPreviousIndex: number | null, currentIndex: number | null, state: ChangeOperationState<T, R, C>) { }

protected removeItem(removeAt: number, state: ChangeOperationState<T, R, C>) { }

protected moveItem(moveFrom: number, moveTo: number, state: ChangeOperationState<T, R, C>) { }

protected afterOperation(state: ChangeOperationState<T, R, C>) { }

protected updateWithNgridContext(itemArgs: _ViewRepeaterItemInsertArgs<C>) {
itemArgs.context = this.extApi.contextApi._createRowContext(itemArgs.context.$implicit, itemArgs.index) as any;
return itemArgs;
}

// See https://github.com/angular/components/pull/20765
protected patch20765(viewContainerRef: ViewContainerRef, changes: IterableChanges<R>, itemContextFactory: _ViewRepeaterItemContextFactory<T, R, C>,) {
changes.forEachIdentityChange = (fn: (record: IterableChangeRecord<R>) => void) => {
changes.constructor.prototype.forEachIdentityChange.call(changes, (record: IterableChangeRecord<R>) => {
fn(record);
if (this._cachedRenderDefMap.get(record.currentIndex) !== record.item.rowDef) {
viewContainerRef.remove(record.currentIndex);
const insertContext = this.updateWithNgridContext(itemContextFactory(record, null, record.currentIndex));
viewContainerRef.createEmbeddedView(insertContext.templateRef, insertContext.context, insertContext.index);
this._cachedRenderDefMap.set(record.currentIndex, record.item.rowDef);
}
});
}
}

protected patch20765afterOp(state: ChangeOperationState<T, R, C>) {
switch (state.op) {
case _ViewRepeaterOperation.REPLACED:
case _ViewRepeaterOperation.INSERTED:
case _ViewRepeaterOperation.MOVED:
this._cachedRenderDefMap.set(state.record.currentIndex, state.record.item.rowDef);
break;
case _ViewRepeaterOperation.REMOVED:
this._cachedRenderDefMap.delete(state.record.previousIndex);
break;
}
}

}
Loading

0 comments on commit 170c2d4

Please sign in to comment.