Skip to content

Commit

Permalink
feat(cdk/scrolling): add input to enable append-only mode in virtual …
Browse files Browse the repository at this point in the history
…scroll viewport (#22986)
  • Loading branch information
MichaelJamesParsons committed Jun 18, 2021
1 parent 2c57c58 commit 8f052cc
Show file tree
Hide file tree
Showing 10 changed files with 132 additions and 2 deletions.
5 changes: 4 additions & 1 deletion src/cdk/scrolling/fixed-size-virtual-scroll.ts
Expand Up @@ -6,7 +6,10 @@
* found in the LICENSE file at https://angular.io/license
*/

import {coerceNumberProperty, NumberInput} from '@angular/cdk/coercion';
import {
coerceNumberProperty,
NumberInput
} from '@angular/cdk/coercion';
import {Directive, forwardRef, Input, OnChanges} from '@angular/core';
import {Observable, Subject} from 'rxjs';
import {distinctUntilChanged} from 'rxjs/operators';
Expand Down
7 changes: 7 additions & 0 deletions src/cdk/scrolling/scrolling.md
Expand Up @@ -121,3 +121,10 @@ custom strategy by creating a class that implements the `VirtualScrollStrategy`
providing it as the `VIRTUAL_SCROLL_STRATEGY` on the component containing your viewport.

<!-- example(cdk-virtual-scroll-custom-strategy) -->

### Append only mode
Virtual scroll viewports that render nontrivial items may find it more performant to simply append
to the list as the user scrolls without removing rendered views. The `appendOnly` input ensures
views that are already rendered persist in the DOM after they scroll out of view.

<!-- example(cdk-virtual-scroll-append-only) -->
61 changes: 61 additions & 0 deletions src/cdk/scrolling/virtual-scroll-viewport.spec.ts
Expand Up @@ -928,6 +928,34 @@ describe('CdkVirtualScrollViewport', () => {
expect(testComponent.trackBy).toHaveBeenCalled();
}));
});

describe('with append only', () => {
let fixture: ComponentFixture<VirtualScrollWithAppendOnly>;
let testComponent: VirtualScrollWithAppendOnly;
let viewport: CdkVirtualScrollViewport;

beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [ScrollingModule, CommonModule],
declarations: [VirtualScrollWithAppendOnly],
}).compileComponents();
fixture = TestBed.createComponent(VirtualScrollWithAppendOnly);
testComponent = fixture.componentInstance;
viewport = testComponent.viewport;
}));

it('should not remove item that have already been rendered', fakeAsync(() => {
finishInit(fixture);
viewport.setRenderedRange({start: 100, end: 200});
fixture.detectChanges();
flush();
viewport.setRenderedRange({start: 10, end: 50});
fixture.detectChanges();
flush();

expect(viewport.getRenderedRange()).toEqual({start: 0, end: 200});
}));
});
});


Expand Down Expand Up @@ -1182,3 +1210,36 @@ class DelayedInitializationVirtualScroll {
trackBy = jasmine.createSpy('trackBy').and.callFake((item: unknown) => item);
renderVirtualFor = false;
}

@Component({
template: `
<cdk-virtual-scroll-viewport appendOnly itemSize="50">
<div class="item" *cdkVirtualFor="let item of items">{{item}}</div>
</cdk-virtual-scroll-viewport>
`,
styles: [`
.cdk-virtual-scroll-content-wrapper {
display: flex;
flex-direction: column;
}
.cdk-virtual-scroll-viewport {
width: 200px;
height: 200px;
background-color: #f5f5f5;
}
.item {
width: 100%;
height: 50px;
box-sizing: border-box;
border: 1px dashed #ccc;
}
`],
encapsulation: ViewEncapsulation.None
})
class VirtualScrollWithAppendOnly {
@ViewChild(CdkVirtualScrollViewport, {static: true}) viewport: CdkVirtualScrollViewport;
itemSize = 50;
items = Array(20000).fill(0).map((_, i) => i);
}
19 changes: 19 additions & 0 deletions src/cdk/scrolling/virtual-scroll-viewport.ts
Expand Up @@ -37,6 +37,7 @@ import {CdkScrollable, ExtendedScrollToOptions} from './scrollable';
import {VIRTUAL_SCROLL_STRATEGY, VirtualScrollStrategy} from './virtual-scroll-strategy';
import {ViewportRuler} from './viewport-ruler';
import {CdkVirtualScrollRepeater} from './virtual-scroll-repeater';
import {BooleanInput, coerceBooleanProperty} from '@angular/cdk/coercion';

/** Checks if the given ranges are equal. */
function rangesEqual(r1: ListRange, r2: ListRange): boolean {
Expand Down Expand Up @@ -89,6 +90,19 @@ export class CdkVirtualScrollViewport extends CdkScrollable implements OnInit, O
}
private _orientation: 'horizontal' | 'vertical' = 'vertical';

/**
* Whether rendered items should persist in the DOM after scrolling out of view. By default, items
* will be removed.
*/
@Input()
get appendOnly(): boolean {
return this._appendOnly;
}
set appendOnly(value: boolean) {
this._appendOnly = coerceBooleanProperty(value);
}
private _appendOnly = false;

// Note: we don't use the typical EventEmitter here because we need to subscribe to the scroll
// strategy lazily (i.e. only if the user is actually listening to the events). We do this because
// depending on how the strategy calculates the scrolled index, it may come at a cost to
Expand Down Expand Up @@ -271,6 +285,9 @@ export class CdkVirtualScrollViewport extends CdkScrollable implements OnInit, O
/** Sets the currently rendered range of indices. */
setRenderedRange(range: ListRange) {
if (!rangesEqual(this._renderedRange, range)) {
if (this.appendOnly) {
range = {start: 0, end: Math.max(this._renderedRange.end, range.end)};
}
this._renderedRangeSubject.next(this._renderedRange = range);
this._markChangeDetectionNeeded(() => this._scrollStrategy.onContentRendered());
}
Expand Down Expand Up @@ -431,4 +448,6 @@ export class CdkVirtualScrollViewport extends CdkScrollable implements OnInit, O
this._totalContentWidth =
this.orientation === 'horizontal' ? `${this._totalContentSize}px` : '';
}

static ngAcceptInputType_appendOnly: BooleanInput;
}
@@ -0,0 +1,9 @@
.example-viewport {
height: 200px;
width: 200px;
border: 1px solid black;
}

.example-item {
height: 50px;
}
@@ -0,0 +1,3 @@
<cdk-virtual-scroll-viewport appendOnly itemSize="50" class="example-viewport">
<div *cdkVirtualFor="let item of items" class="example-item">{{item}}</div>
</cdk-virtual-scroll-viewport>
@@ -0,0 +1,12 @@
import {ChangeDetectionStrategy, Component} from '@angular/core';

/** @title Virtual scroll with view recycling disabled. */
@Component({
selector: 'cdk-virtual-scroll-append-only-example',
styleUrls: ['cdk-virtual-scroll-append-only-example.css'],
templateUrl: 'cdk-virtual-scroll-append-only-example.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CdkVirtualScrollAppendOnlyExample {
items = Array.from({length: 100000}).map((_, i) => `Item #${i}`);
}
5 changes: 5 additions & 0 deletions src/components-examples/cdk/scrolling/index.ts
@@ -1,5 +1,8 @@
import {ScrollingModule} from '@angular/cdk/scrolling';
import {NgModule} from '@angular/core';
import {
CdkVirtualScrollAppendOnlyExample
} from './cdk-virtual-scroll-append-only/cdk-virtual-scroll-append-only-example';
import {
CdkVirtualScrollContextExample
} from './cdk-virtual-scroll-context/cdk-virtual-scroll-context-example';
Expand All @@ -24,6 +27,7 @@ import {
} from './cdk-virtual-scroll-template-cache/cdk-virtual-scroll-template-cache-example';

export {
CdkVirtualScrollAppendOnlyExample,
CdkVirtualScrollContextExample,
CdkVirtualScrollCustomStrategyExample,
CdkVirtualScrollDataSourceExample,
Expand All @@ -35,6 +39,7 @@ export {
};

const EXAMPLES = [
CdkVirtualScrollAppendOnlyExample,
CdkVirtualScrollContextExample,
CdkVirtualScrollCustomStrategyExample,
CdkVirtualScrollDataSourceExample,
Expand Down
8 changes: 8 additions & 0 deletions src/dev-app/virtual-scroll/virtual-scroll-demo.html
Expand Up @@ -170,3 +170,11 @@ <h2>Use with <code>&lt;table&gt;</code></h2>
</tr>
</table>
</cdk-virtual-scroll-viewport>

<h2>Append only</h2>
<cdk-virtual-scroll-viewport class="demo-viewport" appendOnly [itemSize]="50">
<div *cdkVirtualFor="let size of fixedSizeData; let i = index" class="demo-item"
[style.height.px]="size">
Item #{{i}} - ({{size}}px)
</div>
</cdk-virtual-scroll-viewport>
5 changes: 4 additions & 1 deletion tools/public_api_guard/cdk/scrolling.d.ts
Expand Up @@ -120,6 +120,8 @@ export declare class CdkVirtualScrollViewport extends CdkScrollable implements O
_contentWrapper: ElementRef<HTMLElement>;
_totalContentHeight: string;
_totalContentWidth: string;
get appendOnly(): boolean;
set appendOnly(value: boolean);
elementRef: ElementRef<HTMLElement>;
get orientation(): 'horizontal' | 'vertical';
set orientation(orientation: 'horizontal' | 'vertical');
Expand All @@ -143,7 +145,8 @@ export declare class CdkVirtualScrollViewport extends CdkScrollable implements O
setRenderedContentOffset(offset: number, to?: 'to-start' | 'to-end'): void;
setRenderedRange(range: ListRange): void;
setTotalContentSize(size: number): void;
static ɵcmp: i0.ɵɵComponentDeclaration<CdkVirtualScrollViewport, "cdk-virtual-scroll-viewport", never, { "orientation": "orientation"; }, { "scrolledIndexChange": "scrolledIndexChange"; }, never, ["*"]>;
static ngAcceptInputType_appendOnly: BooleanInput;
static ɵcmp: i0.ɵɵComponentDeclaration<CdkVirtualScrollViewport, "cdk-virtual-scroll-viewport", never, { "orientation": "orientation"; "appendOnly": "appendOnly"; }, { "scrolledIndexChange": "scrolledIndexChange"; }, never, ["*"]>;
static ɵfac: i0.ɵɵFactoryDeclaration<CdkVirtualScrollViewport, [null, null, null, { optional: true; }, { optional: true; }, null, null]>;
}

Expand Down

0 comments on commit 8f052cc

Please sign in to comment.