Skip to content

Commit 8f052cc

Browse files
feat(cdk/scrolling): add input to enable append-only mode in virtual scroll viewport (#22986)
1 parent 2c57c58 commit 8f052cc

File tree

10 files changed

+132
-2
lines changed

10 files changed

+132
-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+
### Append only mode
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 removing rendered views. The `appendOnly` input ensures
128+
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: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -928,6 +928,34 @@ 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 not remove item that have already been rendered', fakeAsync(() => {
948+
finishInit(fixture);
949+
viewport.setRenderedRange({start: 100, end: 200});
950+
fixture.detectChanges();
951+
flush();
952+
viewport.setRenderedRange({start: 10, end: 50});
953+
fixture.detectChanges();
954+
flush();
955+
956+
expect(viewport.getRenderedRange()).toEqual({start: 0, end: 200});
957+
}));
958+
});
931959
});
932960

933961

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

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

Lines changed: 19 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,19 @@ export class CdkVirtualScrollViewport extends CdkScrollable implements OnInit, O
8990
}
9091
private _orientation: 'horizontal' | 'vertical' = 'vertical';
9192

93+
/**
94+
* Whether rendered items should persist in the DOM after scrolling out of view. By default, items
95+
* will be removed.
96+
*/
97+
@Input()
98+
get appendOnly(): boolean {
99+
return this._appendOnly;
100+
}
101+
set appendOnly(value: boolean) {
102+
this._appendOnly = coerceBooleanProperty(value);
103+
}
104+
private _appendOnly = false;
105+
92106
// Note: we don't use the typical EventEmitter here because we need to subscribe to the scroll
93107
// strategy lazily (i.e. only if the user is actually listening to the events). We do this because
94108
// depending on how the strategy calculates the scrolled index, it may come at a cost to
@@ -271,6 +285,9 @@ export class CdkVirtualScrollViewport extends CdkScrollable implements OnInit, O
271285
/** Sets the currently rendered range of indices. */
272286
setRenderedRange(range: ListRange) {
273287
if (!rangesEqual(this._renderedRange, range)) {
288+
if (this.appendOnly) {
289+
range = {start: 0, end: Math.max(this._renderedRange.end, range.end)};
290+
}
274291
this._renderedRangeSubject.next(this._renderedRange = range);
275292
this._markChangeDetectionNeeded(() => this._scrollStrategy.onContentRendered());
276293
}
@@ -431,4 +448,6 @@ export class CdkVirtualScrollViewport extends CdkScrollable implements OnInit, O
431448
this._totalContentWidth =
432449
this.orientation === 'horizontal' ? `${this._totalContentSize}px` : '';
433450
}
451+
452+
static ngAcceptInputType_appendOnly: BooleanInput;
434453
}
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+
}
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>
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)