-
Notifications
You must be signed in to change notification settings - Fork 6.8k
feat(virtual-scroll): fixed size virtual scroll #9316
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
src/cdk/scrolling/for-of.ts
Outdated
}) | ||
export class CdkForOf<T> implements CollectionViewer, DoCheck, OnDestroy { | ||
/** Emits when the rendered view of the data changes. */ | ||
viewChange = new Subject<Range>(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rename this to viewChanged
? Otherwise it could introduce an unintentional two-way binding if we decided to make this an output and add an input called view
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This has to be called viewChange
because it's implementing CollectionViewer
|
||
/** The offset of the rendered portion of the data from the start. */ | ||
get renderedContentOffset() { return this._renderedContentOffset; } | ||
set renderedContentOffset(offset) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add a type to the param?
Promise.resolve().then(() => { | ||
this._viewportSize = this.orientation === 'horizontal' ? | ||
this.elementRef.nativeElement.clientWidth : this.elementRef.nativeElement.clientHeight; | ||
fromEvent(this.elementRef.nativeElement, 'scroll').subscribe(() => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This one should be run outside the NgZone
.
} | ||
|
||
ngOnInit() { | ||
Promise.resolve().then(() => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rather than wrapping it in a promise, have you tried running it in one of the later lifecycle hooks?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tried, it seemed like they were not late enough and I had to use the promise
src/cdk/scrolling/for-of.ts
Outdated
// Mark the removed indices so we can recycle their views. | ||
changes.forEachRemovedItem(record => { | ||
this._templateCache.push(previousViews[record.previousIndex!]); | ||
delete previousViews[record.previousIndex!]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using delete
here won't reindex the array, is that intended? Otherwise it might be better to use splice
to avoid some unintended side-effects. Same goes for the other delete
usages.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes this is intentional. I need each element to be at its correct original index so it matches up with the index in the IterableChangeRecord. I then use delete
to mark the view as having been handled in some way (cached or queued up for re-insertion).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it work to to null
? That's generally much faster than using delete
src/cdk/scrolling/for-of.ts
Outdated
} | ||
|
||
/** Apply changes to the DOM. */ | ||
private _applyChanges(changes: IterableChanges<T> | null) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This method is getting kinda long, consider splitting it into a few smaller ones?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed, it does quite a lot.
src/cdk/collections/data-source.ts
Outdated
@@ -7,8 +7,10 @@ | |||
*/ | |||
|
|||
import {Observable} from 'rxjs/Observable'; | |||
import {of} from 'rxjs/observable/of'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should be import {of as observableOf} from 'rxjs/observable/of'
src/cdk/collections/data-source.ts
Outdated
|
||
|
||
/** DataSource wrapper for a native array. */ | ||
export class ArrayDataSource<T> implements DataSource<T> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Move this to its own file?
src/cdk/collections/data-source.ts
Outdated
|
||
|
||
/** DataSource wrapper for a native array. */ | ||
export class ArrayDataSource<T> implements DataSource<T> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe call this StaticArrayDataSource
, since there's no way for it to emit once set?
src/cdk/scrolling/for-of.ts
Outdated
import {switchMap} from 'rxjs/operators/switchMap'; | ||
import {Subject} from 'rxjs/Subject'; | ||
import {CdkVirtualScrollViewport} from './virtual-scroll-viewport'; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should these changes go into cdk-experimental? That will give us a way to publish and get feedback without committing to a final API
src/cdk/scrolling/for-of.ts
Outdated
* container. | ||
*/ | ||
@Directive({ | ||
selector: '[cdkFor][cdkForOf]', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd propose [cdkVirtualFor][cdkVirtualForOf]
. Without the extra term, there's nothing that communicates how this is different from ngFor
.
(with that name propagated throughout the rest of the code)
src/cdk/scrolling/for-of.ts
Outdated
|
||
get even(): boolean { return this.index % 2 === 0; } | ||
|
||
get odd(): boolean { return !this.even; } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For the table we avoided making these getters (CdkCellOutletRowContext
) to avoid the relatively large payload size they generate, especially since these fields would only need to be written once. Avoiding a concrete class makes it a bit smaller, too.
get totalContentSize() { return this._totalContentSize; } | ||
set totalContentSize(size: number) { | ||
if (this._totalContentSize != size) { | ||
this._ngZone.run(() => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Comment that explains why you need to re-enter the zone here?
(and elsewhere)
|
||
/** The total size of all content, including content that is not currently rendered. */ | ||
get totalContentSize() { return this._totalContentSize; } | ||
set totalContentSize(size: number) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These properties shouldn't need to be be getter/setters since they're not @Input
s for the component (could have an update method, which is more obviously side-effecty)
/** A stream that emits whenever the rendered range changes. */ | ||
renderedRangeStream: Observable<Range> = this._renderedRangeSubject.asObservable(); | ||
|
||
_renderedContentTransform: string; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Description?
@Inject(VIRTUAL_SCROLL_STRATEGY) private _scrollStrategy: VirtualScrollStrategy) {} | ||
|
||
/** Connect a `CdkForOf` to this viewport. */ | ||
connect(forOf: CdkForOf<any>) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This isn't formalized anywhere, but I feel that attach
and detach
are more appropriate for components that interact than connect
/disconnect
} | ||
|
||
this._connected = true; | ||
forOf.dataStream.pipe(takeUntil(this._disconnectSubject)).subscribe(data => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add a comment that explains what's happening here?
0cb50f0
to
b678af4
Compare
So there's good news and bad news. 👍 The good news is that everyone that needs to sign a CLA (the pull request submitter and all commit authors) have done so. Everything is all good there. 😕 The bad news is that it appears that one or more commits were authored by someone other than the pull request submitter. We need to confirm that all authors are ok with their commits being contributed to this project. Please have them confirm that here in the pull request. Note to project maintainer: This is a terminal state, meaning the |
48c0a70
to
767916b
Compare
767916b
to
fd0f947
Compare
src/cdk/scrolling/virtual-for-of.ts
Outdated
|
||
ngDoCheck() { | ||
if (this._differ && this._needsUpdate) { | ||
const changes = this._differ.diff(this._renderedItems); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is really pricey, is there a more reactive way we can do this updating?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we could differentiate between needing an update due to scrolling and rendering a new portion of the list (can do something simpler) vs needing an update due to the underlying data changing (in which case I think we'd have to do this diff). I added a TODO to add some logic for that. Also added a TODO to calculate the minimum set of remove & inserts since I'm currently just removing all of the views and re-inserting them in the right order
if (this._renderedContentOffset != offset) { | ||
this._ngZone.run(() => { | ||
this._renderedContentOffset = offset; | ||
this._renderedContentTransform = this.orientation === 'horizontal' ? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Might want to do browser prefix test here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We only have to support back to IE11 (+ latest 2 versions of other browsers) so should be ok
this._renderedContentOffset = offset; | ||
this._renderedContentTransform = this.orientation === 'horizontal' ? | ||
`translateX(${offset}px)`: `translateY(${offset}px)`; | ||
this._changeDetectorRef.markForCheck(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd consider just doing detectChanges
to get it to run immediately.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've added a TODO to investigate this. I'd like to talk to some folks on the core team who know more about change detection than me
if (!this._rangesEqual(this._renderedRange, range)) { | ||
this._ngZone.run(() => { | ||
this._renderedRangeSubject.next(this._renderedRange = range); | ||
this._changeDetectorRef.markForCheck(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd consider just doing detectChanges
to get it to run immediately.
if (this._totalContentSize != size) { | ||
this._ngZone.run(() => { | ||
this._totalContentSize = size; | ||
this._changeDetectorRef.markForCheck(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd consider just doing detectChanges
to get it to run immediately.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In other projects, I was running markForCheck
in a lot of places, when I switched it to detectChanges
I saw a huge perf jump when scrolling.
get renderedContentOffset(): number { return this._renderedContentOffset; } | ||
set renderedContentOffset(offset: number) { | ||
if (this._renderedContentOffset != offset) { | ||
this._ngZone.run(() => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd try to avoid running this in a zone.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The way its set up right now I need to, so the markForCheck
will work, but I'll investigate calling detectChanges
directly
}) | ||
export class CdkVirtualScrollFixedSize implements OnChanges { | ||
/** The size of the items in the list. */ | ||
@Input() itemSize = 20; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be nice if you could make this accept a function too where a user could pass a callback that accepted the current item they were iterating over to return a custom height for that element. For example:
itemSize = (item) => item.type === 'description' ? 50 : 20;
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep that's something I want to add in a future PR
* Or null if no DOM elements are rendered for the given index. | ||
* @throws If the given index is not in the rendered range. | ||
*/ | ||
measureClientRect(index: number): ClientRect | null { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You might want to add a window resize function to update dims on window resizes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Responding to resize is a feature I plan to add, but not in this PR
src/cdk/scrolling/virtual-for-of.ts
Outdated
|
||
/** Update the `CdkVirtualForOfContext` for all views. */ | ||
private _updateContext() { | ||
for (let i = 0, len = this._viewContainerRef.length; i < len; i++) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Given the sensitivity of perf here, you might want to consider a faster loop technique. https://jsperf.com/loops/33
c88df3e
to
63c6051
Compare
63c6051
to
7b5dc1e
Compare
7b5dc1e
to
33cb26f
Compare
Thanks for the feedback everyone. I've put a number of TODOs for things I didn't get to in this PR. I'm going to merge this into the virtual-scroll feature branch now, but there will be plenty more chances to comment before it gets merged to master. |
* feat(virtual-scroll): fixed size virtual scroll * address some comments * change VirtualScrollStrategy interface a bit * address some more comments * fix lint & build
* feat(virtual-scroll): fixed size virtual scroll * address some comments * change VirtualScrollStrategy interface a bit * address some more comments * fix lint & build
* feat(virtual-scroll): fixed size virtual scroll * address some comments * change VirtualScrollStrategy interface a bit * address some more comments * fix lint & build
* feat(virtual-scroll): fixed size virtual scroll * address some comments * change VirtualScrollStrategy interface a bit * address some more comments * fix lint & build
* feat(virtual-scroll): fixed size virtual scroll * address some comments * change VirtualScrollStrategy interface a bit * address some more comments * fix lint & build
* feat(virtual-scroll): fixed size virtual scroll (#9316) * feat(virtual-scroll): fixed size virtual scroll * address some comments * change VirtualScrollStrategy interface a bit * address some more comments * fix lint & build * chore: move virtual-scroll to cdk-experimental (#9974) * virtual-scroll: simplify scroll listener logic (#10102) * virtual-scroll: only move views that need to be moved (#10099) * virtual-scroll: only move views that need to be moved * address comments * virtual-scroll: switch `throttleTime` to `sampleTime` (#10179) * virtual-scroll: switch throttleTime to sampleTime * add comment * virtual-scroll: allow user to pass `Observable<T[]>` (#10158) * virtual-scroll: rename `Range` to `ListRange` to avoid confusion with native `Range` (#10220) * virtual-scroll: add autosize scroll strategy (#10219) * rename fixed size virtual scroll directive * add autosize virtual scroll strategy * add item size estimator class * add logic for jumping rendered content based on scroll position * address comments * virtual-scroll: add `onContentRendered` hook to `VirtualScrollStrategy` (#10290) * virtual-scroll: add `onContentRendered` hook to `VirtualScrollStrategy` * address comemnts * virtual-scroll: add incremental scroll logic in `AutosizeVirtualScrollStrategy` (#10504) * virtual-scroll: add incremental scroll logic in `AutosizeVirtualScrollStrategy`. This still has a couple issues that need to be ironed out and it doesn't have the code for correcting the error between the predicted and actual scroll position. (See various TODOs for additional things that need work). * fix lint * address comments * address comments * fix bazel * fix devapp * fix lint * cleanup * virtual-scroll: rewrite offset in terms of "to-top" and fix a bug where items were removed too soon (#10986) * rewrite offsets to the end of the rendered content as offsets to the start * add some more autosize demos for testing * make sure not to remove too many items * address comments * virtual-scroll: address amcdnl's feedback (#10988) * rewrite offsets to the end of the rendered content as offsets to the start * add some more autosize demos for testing * make sure not to remove too many items * virtual-scroll: address amcdnl's feedback * virtual-scroll: fix updating when data changes and add trackBy demos (#11085) * virtual-scroll: add logic to correct the scroll position as user move… (#11137) * virtual-scroll: add logic to correct the scroll position as user moves toward the top * address comments * fix(scrolling): adds right to fix pushed content (#11192) * fix(scrolling): adds right to fix pushed content * chore: address feedback * chore: address pr feedback * virtual-scroll: add tests for fixed size items (#11255) * virtual-scroll: add tests for fixed size items * address comments * fix bug in fixed size strategy * fix bazel build * virtual-scroll: add tests for `cdkVirtualFor` logic (#11275) * merge fixed size test components into one * add tests for cdkVirtualFor logic * allow undefined to be explicitly passed as trackBy * fix bazel build * address comments * add some basic tests (#11295) * fix lint * virtual-scroll: add e2e tests for autosize scroll strategy (#11345) * set up virtual scroll page in e2e app * add gulp task for e2e:watch * add e2e tests for autosize * address comments * address comments * fix lint and tests * fix bad rebase * fix(scrolling): scrollbar jump when data change on scroll (#11193) * fix(scrolling): data change not updating size * chore: remove todo * chore: more performant fix for jump
This issue has been automatically locked due to inactivity. Read more about our automatic conversation locking policy. This action has been performed automatically by a bot. |
No description provided.