Skip to content

Commit 8daaf4d

Browse files
crisbetojosephperrott
authored andcommitted
fix(scrolling): virtual scroll throw off if directive injects ViewContainerRef (#16137)
Fixes the virtual scrolling being thrown off and rendering the items in a wrong sequence, if there's a directive on the items that injects `ViewContainerRef`. The issue is due to the way we insert new views in a particular index. Rather than inserting the item at the index directly, we insert it and then move it into place. It looks like this behavior, coupled with Angular adding an extra comment node if something injects the `ViewContainerRef` causes the insertion order to be messed up. Fixes #16130.
1 parent 8c4f25f commit 8daaf4d

File tree

2 files changed

+86
-15
lines changed

2 files changed

+86
-15
lines changed

src/cdk/scrolling/virtual-for-of.ts

+14-14
Original file line numberDiff line numberDiff line change
@@ -357,20 +357,20 @@ export class CdkVirtualForOf<T> implements CollectionViewer, DoCheck, OnDestroy
357357

358358
/** Creates a new embedded view and moves it to the given index */
359359
private _createEmbeddedViewAt(index: number): EmbeddedViewRef<CdkVirtualForOfContext<T>> {
360-
const view = this._viewContainerRef.createEmbeddedView(this._template, {
361-
$implicit: null!,
362-
cdkVirtualForOf: this._cdkVirtualForOf,
363-
index: -1,
364-
count: -1,
365-
first: false,
366-
last: false,
367-
odd: false,
368-
even: false
369-
});
370-
if (index < this._viewContainerRef.length) {
371-
this._viewContainerRef.move(view, index);
372-
}
373-
return view;
360+
// Note that it's important that we insert the item directly at the proper index,
361+
// rather than inserting it and the moving it in place, because if there's a directive
362+
// on the same node that injects the `ViewContainerRef`, Angular will insert another
363+
// comment node which can throw off the move when it's being repeated for all items.
364+
return this._viewContainerRef.createEmbeddedView(this._template, {
365+
$implicit: null!,
366+
cdkVirtualForOf: this._cdkVirtualForOf,
367+
index: -1,
368+
count: -1,
369+
first: false,
370+
last: false,
371+
odd: false,
372+
even: false
373+
}, index);
374374
}
375375

376376
/** Inserts a recycled view from the cache at the given index. */

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

+72-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ import {
1212
NgZone,
1313
TrackByFunction,
1414
ViewChild,
15-
ViewEncapsulation
15+
ViewEncapsulation,
16+
Directive,
17+
ViewContainerRef
1618
} from '@angular/core';
1719
import {async, ComponentFixture, fakeAsync, flush, inject, TestBed} from '@angular/core/testing';
1820
import {animationFrameScheduler, Subject} from 'rxjs';
@@ -797,6 +799,36 @@ describe('CdkVirtualScrollViewport', () => {
797799
'Error: cdk-virtual-scroll-viewport requires the "itemSize" property to be set.');
798800
}));
799801
});
802+
803+
describe('with item that injects ViewContainerRef', () => {
804+
let fixture: ComponentFixture<VirtualScrollWithItemInjectingViewContainer>;
805+
let testComponent: VirtualScrollWithItemInjectingViewContainer;
806+
let viewport: CdkVirtualScrollViewport;
807+
808+
beforeEach(async(() => {
809+
TestBed.configureTestingModule({
810+
imports: [ScrollingModule],
811+
declarations: [VirtualScrollWithItemInjectingViewContainer, InjectsViewContainer],
812+
}).compileComponents();
813+
}));
814+
815+
beforeEach(() => {
816+
fixture = TestBed.createComponent(VirtualScrollWithItemInjectingViewContainer);
817+
testComponent = fixture.componentInstance;
818+
viewport = testComponent.viewport;
819+
});
820+
821+
it('should render the values in the correct sequence when an item is ' +
822+
'injecting ViewContainerRef', fakeAsync(() => {
823+
finishInit(fixture);
824+
825+
const contentWrapper =
826+
viewport.elementRef.nativeElement.querySelector('.cdk-virtual-scroll-content-wrapper')!;
827+
828+
expect(Array.from(contentWrapper.children).map(child => child.textContent!.trim()))
829+
.toEqual(['0', '1', '2', '3', '4', '5', '6', '7']);
830+
}));
831+
});
800832
});
801833

802834

@@ -938,3 +970,42 @@ class FixedSizeVirtualScrollWithRtlDirection {
938970
class VirtualScrollWithNoStrategy {
939971
items = [];
940972
}
973+
974+
@Directive({
975+
selector: '[injects-view-container]'
976+
})
977+
class InjectsViewContainer {
978+
constructor(public viewContainerRef: ViewContainerRef) {
979+
}
980+
}
981+
982+
@Component({
983+
template: `
984+
<cdk-virtual-scroll-viewport itemSize="50">
985+
<div injects-view-container class="item" *cdkVirtualFor="let item of items">{{item}}</div>
986+
</cdk-virtual-scroll-viewport>
987+
`,
988+
styles: [`
989+
.cdk-virtual-scroll-content-wrapper {
990+
display: flex;
991+
flex-direction: column;
992+
}
993+
994+
.cdk-virtual-scroll-viewport {
995+
width: 200px;
996+
height: 200px;
997+
}
998+
999+
.item {
1000+
width: 100%;
1001+
height: 50px;
1002+
}
1003+
`],
1004+
encapsulation: ViewEncapsulation.None
1005+
})
1006+
class VirtualScrollWithItemInjectingViewContainer {
1007+
@ViewChild(CdkVirtualScrollViewport, {static: true}) viewport: CdkVirtualScrollViewport;
1008+
itemSize = 50;
1009+
items = Array(20000).fill(0).map((_, i) => i);
1010+
}
1011+

0 commit comments

Comments
 (0)