Skip to content

Commit 82bdbb8

Browse files
feat(cdk/virtual-scroll): Infinite scroll mode
1 parent 30cfd7d commit 82bdbb8

File tree

10 files changed

+128
-2
lines changed

10 files changed

+128
-2
lines changed

src/cdk/scrolling/fixed-size-virtual-scroll.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {coerceNumberProperty, NumberInput} from '@angular/cdk/coercion';
9+
import {
10+
coerceNumberProperty,
11+
NumberInput
12+
} from '@angular/cdk/coercion';
1013
import {Directive, forwardRef, Input, OnChanges} from '@angular/core';
1114
import {Observable, Subject} from 'rxjs';
1215
import {distinctUntilChanged} from 'rxjs/operators';

src/cdk/scrolling/scrolling.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,3 +121,10 @@ custom strategy by creating a class that implements the `VirtualScrollStrategy`
121121
providing it as the `VIRTUAL_SCROLL_STRATEGY` on the component containing your viewport.
122122

123123
<!-- example(cdk-virtual-scroll-custom-strategy) -->
124+
125+
### Disable view recycling
126+
Virtual scroll viewports that render nontrivial items may find it more performant to simply append
127+
to the list as the user scrolls without recycling rendered views. The `appendOnly` input disables
128+
view recycling so views that are already rendered persist in the DOM after they scroll out of view.
129+
130+
<!-- example(cdk-virtual-scroll-append-only) -->

src/cdk/scrolling/virtual-scroll-viewport.spec.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -928,6 +928,31 @@ describe('CdkVirtualScrollViewport', () => {
928928
expect(testComponent.trackBy).toHaveBeenCalled();
929929
}));
930930
});
931+
932+
describe('with append only', () => {
933+
let fixture: ComponentFixture<VirtualScrollWithAppendOnly>;
934+
let testComponent: VirtualScrollWithAppendOnly;
935+
let viewport: CdkVirtualScrollViewport;
936+
937+
beforeEach(waitForAsync(() => {
938+
TestBed.configureTestingModule({
939+
imports: [ScrollingModule, CommonModule],
940+
declarations: [VirtualScrollWithAppendOnly],
941+
}).compileComponents();
942+
fixture = TestBed.createComponent(VirtualScrollWithAppendOnly);
943+
testComponent = fixture.componentInstance;
944+
viewport = testComponent.viewport;
945+
}));
946+
947+
it('should override rendered range start', fakeAsync(() => {
948+
finishInit(fixture);
949+
viewport.setRenderedRange({start: 2, end: 3});
950+
fixture.detectChanges();
951+
flush();
952+
953+
expect(viewport.getRenderedRange()).toEqual({start: 0, end: 3});
954+
}));
955+
});
931956
});
932957

933958

@@ -1182,3 +1207,36 @@ class DelayedInitializationVirtualScroll {
11821207
trackBy = jasmine.createSpy('trackBy').and.callFake((item: unknown) => item);
11831208
renderVirtualFor = false;
11841209
}
1210+
1211+
@Component({
1212+
template: `
1213+
<cdk-virtual-scroll-viewport appendOnly itemSize="50">
1214+
<div class="item" *cdkVirtualFor="let item of items">{{item}}</div>
1215+
</cdk-virtual-scroll-viewport>
1216+
`,
1217+
styles: [`
1218+
.cdk-virtual-scroll-content-wrapper {
1219+
display: flex;
1220+
flex-direction: column;
1221+
}
1222+
1223+
.cdk-virtual-scroll-viewport {
1224+
width: 200px;
1225+
height: 200px;
1226+
background-color: #f5f5f5;
1227+
}
1228+
1229+
.item {
1230+
width: 100%;
1231+
height: 50px;
1232+
box-sizing: border-box;
1233+
border: 1px dashed #ccc;
1234+
}
1235+
`],
1236+
encapsulation: ViewEncapsulation.None
1237+
})
1238+
class VirtualScrollWithAppendOnly {
1239+
@ViewChild(CdkVirtualScrollViewport, {static: true}) viewport: CdkVirtualScrollViewport;
1240+
itemSize = 50;
1241+
items = Array(20000).fill(0).map((_, i) => i);
1242+
}

src/cdk/scrolling/virtual-scroll-viewport.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import {CdkScrollable, ExtendedScrollToOptions} from './scrollable';
3737
import {VIRTUAL_SCROLL_STRATEGY, VirtualScrollStrategy} from './virtual-scroll-strategy';
3838
import {ViewportRuler} from './viewport-ruler';
3939
import {CdkVirtualScrollRepeater} from './virtual-scroll-repeater';
40+
import {BooleanInput, coerceBooleanProperty} from '@angular/cdk/coercion';
4041

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

93+
/**
94+
* Disables view recycling so rendered items persist in the DOM even after scrolling out of view.
95+
*/
96+
@Input()
97+
get appendOnly(): boolean {
98+
return this._appendOnly;
99+
}
100+
set appendOnly(value: boolean) {
101+
this._appendOnly = coerceBooleanProperty(value);
102+
}
103+
private _appendOnly = false;
104+
92105
// Note: we don't use the typical EventEmitter here because we need to subscribe to the scroll
93106
// strategy lazily (i.e. only if the user is actually listening to the events). We do this because
94107
// depending on how the strategy calculates the scrolled index, it may come at a cost to
@@ -271,6 +284,9 @@ export class CdkVirtualScrollViewport extends CdkScrollable implements OnInit, O
271284
/** Sets the currently rendered range of indices. */
272285
setRenderedRange(range: ListRange) {
273286
if (!rangesEqual(this._renderedRange, range)) {
287+
if (this.appendOnly) {
288+
range = {...range, start: 0};
289+
}
274290
this._renderedRangeSubject.next(this._renderedRange = range);
275291
this._markChangeDetectionNeeded(() => this._scrollStrategy.onContentRendered());
276292
}
@@ -431,4 +447,6 @@ export class CdkVirtualScrollViewport extends CdkScrollable implements OnInit, O
431447
this._totalContentWidth =
432448
this.orientation === 'horizontal' ? `${this._totalContentSize}px` : '';
433449
}
450+
451+
static ngAcceptInputType_appendOnly: BooleanInput;
434452
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
.example-viewport {
2+
height: 200px;
3+
width: 200px;
4+
border: 1px solid black;
5+
}
6+
7+
.example-item {
8+
height: 50px;
9+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<cdk-virtual-scroll-viewport appendOnly itemSize="50" class="example-viewport">
2+
<div *cdkVirtualFor="let item of items" class="example-item">{{item}}</div>
3+
</cdk-virtual-scroll-viewport>
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import {ChangeDetectionStrategy, Component} from '@angular/core';
2+
3+
/** @title Virtual scroll with view recycling disabled. */
4+
@Component({
5+
selector: 'cdk-virtual-scroll-append-only-example',
6+
styleUrls: ['cdk-virtual-scroll-append-only-example.css'],
7+
templateUrl: 'cdk-virtual-scroll-append-only-example.html',
8+
changeDetection: ChangeDetectionStrategy.OnPush,
9+
})
10+
export class CdkVirtualScrollAppendOnlyExample {
11+
items = Array.from({length: 100000}).map((_, i) => `Item #${i}`);
12+
}

src/components-examples/cdk/scrolling/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import {ScrollingModule} from '@angular/cdk/scrolling';
22
import {NgModule} from '@angular/core';
3+
import {
4+
CdkVirtualScrollAppendOnlyExample
5+
} from './cdk-virtual-scroll-append-only/cdk-virtual-scroll-append-only-example';
36
import {
47
CdkVirtualScrollContextExample
58
} from './cdk-virtual-scroll-context/cdk-virtual-scroll-context-example';
@@ -24,6 +27,7 @@ import {
2427
} from './cdk-virtual-scroll-template-cache/cdk-virtual-scroll-template-cache-example';
2528

2629
export {
30+
CdkVirtualScrollAppendOnlyExample,
2731
CdkVirtualScrollContextExample,
2832
CdkVirtualScrollCustomStrategyExample,
2933
CdkVirtualScrollDataSourceExample,
@@ -35,6 +39,7 @@ export {
3539
};
3640

3741
const EXAMPLES = [
42+
CdkVirtualScrollAppendOnlyExample,
3843
CdkVirtualScrollContextExample,
3944
CdkVirtualScrollCustomStrategyExample,
4045
CdkVirtualScrollDataSourceExample,

src/dev-app/virtual-scroll/virtual-scroll-demo.html

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,3 +170,11 @@ <h2>Use with <code>&lt;table&gt;</code></h2>
170170
</tr>
171171
</table>
172172
</cdk-virtual-scroll-viewport>
173+
174+
<h2>Append only</h2>
175+
<cdk-virtual-scroll-viewport class="demo-viewport" appendOnly [itemSize]="50">
176+
<div *cdkVirtualFor="let size of fixedSizeData; let i = index" class="demo-item"
177+
[style.height.px]="size">
178+
Item #{{i}} - ({{size}}px)
179+
</div>
180+
</cdk-virtual-scroll-viewport>

tools/public_api_guard/cdk/scrolling.d.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,8 @@ export declare class CdkVirtualScrollViewport extends CdkScrollable implements O
120120
_contentWrapper: ElementRef<HTMLElement>;
121121
_totalContentHeight: string;
122122
_totalContentWidth: string;
123+
get appendOnly(): boolean;
124+
set appendOnly(value: boolean);
123125
elementRef: ElementRef<HTMLElement>;
124126
get orientation(): 'horizontal' | 'vertical';
125127
set orientation(orientation: 'horizontal' | 'vertical');
@@ -143,7 +145,8 @@ export declare class CdkVirtualScrollViewport extends CdkScrollable implements O
143145
setRenderedContentOffset(offset: number, to?: 'to-start' | 'to-end'): void;
144146
setRenderedRange(range: ListRange): void;
145147
setTotalContentSize(size: number): void;
146-
static ɵcmp: i0.ɵɵComponentDeclaration<CdkVirtualScrollViewport, "cdk-virtual-scroll-viewport", never, { "orientation": "orientation"; }, { "scrolledIndexChange": "scrolledIndexChange"; }, never, ["*"]>;
148+
static ngAcceptInputType_appendOnly: BooleanInput;
149+
static ɵcmp: i0.ɵɵComponentDeclaration<CdkVirtualScrollViewport, "cdk-virtual-scroll-viewport", never, { "orientation": "orientation"; "appendOnly": "appendOnly"; }, { "scrolledIndexChange": "scrolledIndexChange"; }, never, ["*"]>;
147150
static ɵfac: i0.ɵɵFactoryDeclaration<CdkVirtualScrollViewport, [null, null, null, { optional: true; }, { optional: true; }, null, null]>;
148151
}
149152

0 commit comments

Comments
 (0)