Skip to content

Commit

Permalink
refactor(ngrid): reset height cache in dynamic virtual scroll
Browse files Browse the repository at this point in the history
  • Loading branch information
shlomiassaf committed Dec 3, 2020
1 parent c3d8e3d commit db28ef2
Show file tree
Hide file tree
Showing 10 changed files with 231 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,29 @@ I> If there is no unique column context support is available but limited.

For example, if we remove the `pIndex` from the example above, each click for sort/pagination will clear the cache since
there is no way for the context to identify and match exiting context to rows.

## Context Pitfalls

Some key points:

- Context is state and managing state, as we all now, is **hard**.
- **nGrid** is a composition of multiple features. Some interact with each other, some are native and some are plugins.

Depending on the complexity of each feature and the areas in which it has effect on, managing the context might be tricky.

For example, filtering is an operation which modifies the existing dataset.
From here, things diverge based on the components used in **nGrid**.

If the datasource implementation handles filtering on the **server** the entire datasource is replaced on each filtering operation.
However, when filtering is done on the existing datasource, it is kept in memory but only a portion of it is actually used.

Each behavior impact the context differently. Filtering on the server will clear the context, filtering on the client will keep it.

Additional features on top of the above? more complexity!

For example, the `Dynamic Virtual Scroll` strategy is sensitive to filtering, regardless of it's origin, other virtual scroll strategies might be less sensitive.
It is all based on the implementation.

In general, virtual scroll operations and different datasource implementations (e.e. Infinite Scroll) might have context specific behaviors.
Read the documentation of the features you use for more information.

Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
#pblTbl
rowReorder columnReorder
blockUi
matSort vScrollDynamic
matSort
vScrollDynamic minBufferPx="300" maxBufferPx="500"
cellTooltip
matCheckboxSelection="selection"
focusMode="row"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export class ComplexDemo1Example {
{ prop: 'gender', width: '50px' },
{ prop: 'birthdate', type: 'date' },
{ prop: 'bio' },
{ prop: 'settings.avatar', width: '40px' },
// { prop: 'settings.avatar', width: '40px' },
{ prop: 'settings.background' },
{ prop: 'settings.timezone' },
{ prop: 'settings.emailFrequency', editable: true },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,10 +118,12 @@ has switched between DOM elements and the close/open event is not in place. It w
to the new row for us. Moreover, if an opened row was scrolled out of view into the virtual void it's height is
stored and reflected in the total, once we scroll back into it, it will be rendered in an open state, again with no animation!

W> The Dynamic Strategy will reset the total size and all size marks to 0 when filtering

> Moving forward, the Dynamic Strategy will update, improving it's accuracy based on experience.
For example, instead of using a default size we can fine-tune it to an average.

## Global Strategy (default)
## Configure A Global / Default Strategy

The global strategy set by default to all instances of **nGrid** is `vScrollAuto`.
If a virtual scroll strategy is not set the global strategy will be used.
Expand Down
2 changes: 1 addition & 1 deletion libs/ngrid/src/lib/grid/context/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -305,13 +305,13 @@ export class ContextApi<T = any> {
}

updateOutOfViewState(rowContext: PblRowContext<T>): void {
const viewPortRect = this.getViewRect();
// This is check should not happen but it turns out it might
// When rendering and updating the grid multiple times and in async it might occur while fast scrolling
// The index of the row is updated to something out-of bounds and the viewRef is null.
// The current known scenario for this is when doing fast infinite scroll and having an open edit cell with `pblCellEditAutoFocus`
// The auto-focus fires after all CD is done but in the meanwhile, if fast scrolling the in-edit cell, it might happen
if (rowContext._attachedRow) {
const viewPortRect = this.getViewRect();
processOutOfView(rowContext, viewPortRect);
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import {Observable, Subject} from 'rxjs';
import {distinctUntilChanged} from 'rxjs/operators';
import { VirtualScrollStrategy, CdkVirtualScrollViewport } from '@angular/cdk/scrolling';

/** 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;

/** The size of the items in the virtually scrolling list. */
private _itemSize: number;

/** The minimum amount of buffer rendered beyond the viewport (in pixels). */
private _minBufferPx: number;

/** The number of buffer items to render beyond the edge of the viewport (in pixels). */
private _maxBufferPx: number;

/**
* @param itemSize The size of the items in the virtually scrolling list.
* @param minBufferPx The minimum amount of buffer (in pixels) before needing to render more
* @param maxBufferPx The amount of buffer (in pixels) to render when rendering more.
*/
constructor(itemSize: number, minBufferPx: number, maxBufferPx: number) {
this._itemSize = itemSize;
this._minBufferPx = minBufferPx;
this._maxBufferPx = maxBufferPx;
}

/**
* Attaches this scroll strategy to a viewport.
* @param viewport The viewport to attach this strategy to.
*/
attach(viewport: CdkVirtualScrollViewport) {
this._viewport = viewport;
this._updateTotalContentSize();
this._updateRenderedRange();
}

/** Detaches this scroll strategy from the currently attached viewport. */
detach() {
this._scrolledIndexChange.complete();
this._viewport = null;
}

/**
* Update the item size and buffer size.
* @param itemSize The size of the items in the virtually scrolling list.
* @param minBufferPx The minimum amount of buffer (in pixels) before needing to render more
* @param maxBufferPx The amount of buffer (in pixels) to render when rendering more.
*/
updateItemAndBufferSize(itemSize: number, minBufferPx: number, maxBufferPx: number) {
if (maxBufferPx < minBufferPx && (typeof ngDevMode === 'undefined' || ngDevMode)) {
throw Error('CDK virtual scroll: maxBufferPx must be greater than or equal to minBufferPx');
}
this._itemSize = itemSize;
this._minBufferPx = minBufferPx;
this._maxBufferPx = maxBufferPx;
this._updateTotalContentSize();
this._updateRenderedRange();
}

/** @docs-private Implemented as part of VirtualScrollStrategy. */
onContentScrolled() {
this._updateRenderedRange();
}

/** @docs-private Implemented as part of VirtualScrollStrategy. */
onDataLengthChanged() {
this._updateTotalContentSize();
this._updateRenderedRange();
}

/** @docs-private Implemented as part of VirtualScrollStrategy. */
onContentRendered() { /* no-op */ }

/** @docs-private Implemented as part of VirtualScrollStrategy. */
onRenderedOffsetChanged() { /* no-op */ }

/**
* Scroll to the offset for the given index.
* @param index The index of the element to scroll to.
* @param behavior The ScrollBehavior to use when scrolling.
*/
scrollToIndex(index: number, behavior: ScrollBehavior): void {
if (this._viewport) {
this._viewport.scrollToOffset(index * this._itemSize, behavior);
}
}

/** Update the viewport's total content size. */
private _updateTotalContentSize() {
if (!this._viewport) {
return;
}

this._viewport.setTotalContentSize(this._viewport.getDataLength() * this._itemSize);
}

/** Update the viewport's rendered range. */
private _updateRenderedRange() {
if (!this._viewport) {
return;
}

const renderedRange = this._viewport.getRenderedRange();
const newRange = {start: renderedRange.start, end: renderedRange.end};
const viewportSize = this._viewport.getViewportSize();
const dataLength = this._viewport.getDataLength();
let scrollOffset = this._viewport.measureScrollOffset();
let firstVisibleIndex = scrollOffset / this._itemSize;

// If user scrolls to the bottom of the list and data changes to a smaller list
if (newRange.end > dataLength) {
// We have to recalculate the first visible index based on new data length and viewport size.
const maxVisibleItems = Math.ceil(viewportSize / this._itemSize);
const newVisibleIndex = Math.max(0,
Math.min(firstVisibleIndex, dataLength - maxVisibleItems));

// If first visible index changed we must update scroll offset to handle start/end buffers
// Current range must also be adjusted to cover the new position (bottom of new list).
if (firstVisibleIndex != newVisibleIndex) {
firstVisibleIndex = newVisibleIndex;
scrollOffset = newVisibleIndex * this._itemSize;
newRange.start = Math.floor(firstVisibleIndex);
}

newRange.end = Math.max(0, Math.min(dataLength, newRange.start + maxVisibleItems));
}

const startBuffer = scrollOffset - newRange.start * this._itemSize;
if (startBuffer < this._minBufferPx && newRange.start != 0) {
const expandStart = Math.ceil((this._maxBufferPx - startBuffer) / this._itemSize);
newRange.start = Math.max(0, newRange.start - expandStart);
newRange.end = Math.min(dataLength,
Math.ceil(firstVisibleIndex + (viewportSize + this._minBufferPx) / this._itemSize));
} else {
const endBuffer = newRange.end * this._itemSize - (scrollOffset + viewportSize);
if (endBuffer < this._minBufferPx && newRange.end != dataLength) {
const expandEnd = Math.ceil((this._maxBufferPx - endBuffer) / this._itemSize);
if (expandEnd > 0) {
newRange.end = Math.min(dataLength, newRange.end + expandEnd);
newRange.start = Math.max(0,
Math.floor(firstVisibleIndex - this._minBufferPx / this._itemSize));
}
}
}

this._viewport.setRenderedRange(newRange);
this._viewport.setRenderedContentOffset(this._itemSize * newRange.start);
this._scrolledIndexChange.next(Math.floor(firstVisibleIndex));
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { FixedSizeVirtualScrollStrategy } from '@angular/cdk/scrolling';
import { PblNgridExtensionApi } from '../../../../../ext/grid-ext-api';
import { PblCdkVirtualScrollViewportComponent } from '../../virtual-scroll-viewport.component';
import { PblNgridVirtualScrollStrategy } from '../types';
import { FixedSizeVirtualScrollStrategy } from './fixed-size-cdk';

declare module '../types' {
interface PblNgridVirtualScrollStrategyMap {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { Observable, Subject } from 'rxjs';
import { distinctUntilChanged } from 'rxjs/operators';
import { PblNgridInternalExtensionApi, PblNgridExtensionApi } from '../../../../../ext/grid-ext-api';
import { PblDataSource } from '../../../../../data-source/data-source';
import { unrx } from '../../../../utils/unrx';
import { PblCdkVirtualScrollViewportComponent } from '../../virtual-scroll-viewport.component';
import { PblNgridVirtualScrollStrategy } from '../types';
import { Sizer } from './sizer';
Expand Down Expand Up @@ -67,6 +69,15 @@ export class PblNgridDynamicVirtualScrollStrategy implements PblNgridVirtualScro

attachExtApi(extApi: PblNgridExtensionApi): void {
this.extApi = extApi as PblNgridInternalExtensionApi;
this.extApi.events
.subscribe( event => {
if (event.kind === 'onDataSource') {
this.onDatasource(event.curr, event.prev);
}
});
if (this.extApi.grid.ds) {
this.onDatasource(this.extApi.grid.ds);
}
}

attach(viewport: PblCdkVirtualScrollViewportComponent): void {
Expand Down Expand Up @@ -113,6 +124,19 @@ export class PblNgridDynamicVirtualScrollStrategy implements PblNgridVirtualScro
}
}

protected onDatasource(curr: PblDataSource, prev?: PblDataSource) {
if (prev) {
unrx.kill(this, prev);
}
if (curr) {
curr.onSourceChanging
.pipe(unrx(this, curr))
.subscribe(() => {
this.sizer.clear();
});
}
}

protected _updateSizeAndRange() {
this._updateTotalContentSize();
this._updateRenderedRange(true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export class SizeGroupCollection {

get length() { return this._groups.length; }

private readonly _groups: SizeGroup[] = [];
private _groups: SizeGroup[] = [];

set(group: SizeGroup) {
const groupIndex = group.groupIndex;
Expand Down Expand Up @@ -42,8 +42,8 @@ export class SizeGroupCollection {
return this.findGroupIndexIndex(groupIndex) > -1;
}

[Symbol.iterator]() {

clear() {
this._groups = [];
}

protected findGroupIndexIndex(groupIndex: number, matchClosest?: boolean) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ export class Sizer {
}
}

clear() {
this.groups.clear();
}

setSize(dsIndex: number, height: number) {
const groupIndex = this.getGroupIndex(dsIndex);

Expand Down

0 comments on commit db28ef2

Please sign in to comment.