Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/cdk/scrolling/fixed-size-virtual-scroll.ts
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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) -->

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's been a while since I've looked at this code, but how is this different to setting the cache size to zero? I was under the impression that if a view can't be cached, it'll be recreated.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the difference is that with cache size zero, it won't reuse already created views, but it will remove them from the DOM. The idea with this change is to have a mode where it never removes anything from the DOM, only adds.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I updated the description to clarify this behavior.

### 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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.example-viewport {
height: 200px;
width: 200px;
border: 1px solid black;
}

.example-item {
height: 50px;
}
Original file line number Diff line number Diff line change
@@ -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>
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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